├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── persistentsearchview ├── .gitignore ├── bintray.gradle ├── build.gradle ├── gradle.properties ├── install.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── cryse │ │ └── widget │ │ └── persistentsearch │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── android │ │ └── support │ │ │ └── v7 │ │ │ └── graphics │ │ │ └── drawable │ │ │ └── SupportDrawerArrowDrawable.java │ ├── io │ │ └── codetail │ │ │ ├── animation │ │ │ ├── RevealAnimator.java │ │ │ ├── SupportAnimator.java │ │ │ ├── SupportAnimatorLollipop.java │ │ │ ├── SupportAnimatorPreL.java │ │ │ └── ViewAnimationUtils.java │ │ │ └── widget │ │ │ ├── RevealFrameLayout.java │ │ │ └── RevealLinearLayout.java │ └── org │ │ └── cryse │ │ └── widget │ │ └── persistentsearch │ │ ├── DefaultVoiceRecognizerDelegate.java │ │ ├── HomeButton.java │ │ ├── LogoView.java │ │ ├── PersistentSearchView.java │ │ ├── RevealViewGroup.java │ │ ├── SearchItem.java │ │ ├── SearchItemAdapter.java │ │ ├── SearchSuggestionsBuilder.java │ │ ├── SimpleSearchListener.java │ │ ├── TextDrawable.java │ │ └── VoiceRecognitionDelegate.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_clear_black.png │ ├── ic_action_mic_black.png │ ├── ic_history_black.png │ └── ic_search_black.png │ ├── drawable-mdpi │ ├── ic_action_clear_black.png │ ├── ic_action_mic_black.png │ ├── ic_history_black.png │ └── ic_search_black.png │ ├── drawable-xhdpi │ ├── ic_action_clear_black.png │ ├── ic_action_mic_black.png │ ├── ic_history_black.png │ └── ic_search_black.png │ ├── drawable-xxhdpi │ ├── ic_action_clear_black.png │ ├── ic_action_mic_black.png │ ├── ic_history_black.png │ └── ic_search_black.png │ ├── drawable-xxxhdpi │ ├── ic_action_clear_black.png │ ├── ic_action_mic_black.png │ ├── ic_history_black.png │ └── ic_search_black.png │ ├── layout │ ├── layout_searchitem.xml │ └── layout_searchview.xml │ └── values │ ├── attrs.xml │ ├── dimens.xml │ └── strings.xml ├── sample-apk └── sample.apk ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── org │ │ └── cryse │ │ └── widget │ │ └── persistentsearch │ │ └── sample │ │ └── ApplicationTest.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── cryse │ │ └── widget │ │ └── persistentsearch │ │ └── sample │ │ ├── MainActivity.java │ │ ├── MenuItemSampleActivity.java │ │ ├── SampleSuggestionsBuilder.java │ │ ├── SearchActivity.java │ │ ├── SearchFragment.java │ │ ├── SearchResult.java │ │ ├── SearchResultAdapter.java │ │ └── SimpleAnimationListener.java │ └── res │ ├── drawable-hdpi │ ├── ic_action_search.png │ └── ic_logo.png │ ├── drawable-mdpi │ ├── ic_action_search.png │ └── ic_logo.png │ ├── drawable-xhdpi │ ├── ic_action_search.png │ └── ic_logo.png │ ├── drawable-xxhdpi │ ├── ic_action_search.png │ └── ic_logo.png │ ├── drawable-xxxhdpi │ ├── ic_action_search.png │ └── ic_logo.png │ ├── layout │ ├── activity_main.xml │ ├── activity_menu_item_sample.xml │ ├── activity_search.xml │ └── layout_item_search_result.xml │ ├── menu │ └── menu_searchview.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ └── values │ ├── strings.xml │ └── styles.xml ├── screenshots ├── screenshot1.png └── screenshot2.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea/workspace.xml 4 | .DS_Store 5 | 6 | # Crashlytics 7 | com_crashlytics_export_strings.xml 8 | 9 | # built application files 10 | *.ap_ 11 | 12 | # files for the dex VM 13 | *.dex 14 | 15 | # Java class files 16 | *.class 17 | 18 | # generated files 19 | bin/ 20 | gen/ 21 | build/ 22 | 23 | # Local configuration file (sdk path, etc) 24 | local.properties 25 | 26 | # Eclipse project files 27 | .classpath 28 | .project 29 | 30 | # Proguard folder generated by Eclipse 31 | proguard/ 32 | 33 | # Android Studio 34 | .idea/ 35 | .gradle 36 | /*/local.properties 37 | /*/out 38 | /*/*/build 39 | /*/*/production 40 | *.iml 41 | *.iws 42 | *.ipr 43 | *~ 44 | *.swp 45 | 46 | # SigningConfig 47 | signing.properties 48 | *.keystore 49 | javadoc/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DEPRECATED: THIS PROJECT IS NO LONGER MAINTAINED 2 | # Persistent Search 3 | A library that implements Google Play like PersistentSearch view. 4 | 5 | - API: 14+ 6 | - 3 modes: Toolbar with drawer button, MenuItem and Toolbar with back button 7 | - MenuItem mode reveal animation (Using [ozodrukh/CircularReveal](https://github.com/ozodrukh/CircularReveal)) 8 | - Voice recognition support. 9 | - Use `android.support.v7.widget.CardView` to draw background and shadow, you can set `persistentSV_searchCardElevation` to modify shadow size. 10 | 11 | ## Demo 12 | 13 | 14 | Get it on Google Play 16 | 17 | 18 | Or APK [Here](https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/master/sample-apk/sample.apk) 19 | 20 | ## Integration 21 | 22 | You can import from `jCenter()`: 23 | 24 | `compile 'org.cryse.widget:persistentsearchview:1.0.4@aar'` 25 | 26 | ## Custom Attributes 27 | 28 | ```xml 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | 50 | Note: 51 | 52 | - CardView content height is 48dp 53 | - `android:elevation` attribute only decide z-axis position of SearchView, but not draw any shadow, shadow size is decided by `app:persistentSV_searchCardElevation`. 54 | - the CardView vertical padding equals (actionBarSize - cardViewHeight) / 2 55 | - the CardView horizontal padding have two values: 56 | - Toolbar mode: 8dp 57 | - MenuItem mode: 4dp 58 | 59 | you can change `persistentSV_customToolbarHeight` to change vertical padding (for example, 64dp height will get `(64 - 48) / 2 = 8dp` vertical padding). 60 | 61 | ## Sample Usages 62 | ### Display as Toolbar with drawer(burger) button 63 | 64 | `app:persistentSV_displayMode="toolbar"` 65 | `app:persistentSV_homeButtonMode="burger"` 66 | 67 | ```xml 68 | 83 | ``` 84 | 85 | ### Display as Toolbar with back arrow 86 | 87 | Change the attribute in layout file: 88 | 89 | `app:persistentSV_displayMode="toolbar"` 90 | `app:persistentSV_homeButtonMode="arrow"` 91 | 92 | ### Display as MenuItem 93 | 94 | Change the attributes in layout file: 95 | 96 | `android:visibility="gone"` 97 | `app:persistentSV_displayMode="menuItem"` 98 | 99 | When you need to show it, call `openSearch(View view)` to show SearchView, start position is determinate by `view` param, for example: 100 | ```java 101 | View menuItemView = findViewById(R.id.action_search); 102 | mSearchView.openSearch(menuItemView); 103 | ``` 104 | When you need to hide it, call `searchView.closeSearch()` 105 | 106 | ### Voice Recognition 107 | 108 | If you want use voice recognition, just check the availability and setup: 109 | ```java 110 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE); 111 | if(delegate.isVoiceRecognitionAvailable()) { 112 | mSearchView.setVoiceRecognitionDelegate(delegate); 113 | } 114 | ``` 115 | then in onActivityResult(): 116 | ```java 117 | @Override 118 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 119 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) { 120 | ArrayList matches = data 121 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); 122 | mSearchView.populateEditText(matches); // Set result to PersistentSearchView 123 | } 124 | super.onActivityResult(requestCode, resultCode, data); 125 | } 126 | ``` 127 | 128 | If you don't want to use default voice recognizer, you could inherit from abstract class VoiceRecognitionDelegate and implement your own recognizer. 129 | 130 | ## Screenshot 131 | 132 | ![Screenshot1](https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/master/screenshots/screenshot1.png) 133 | ![Screenshot2](https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/master/screenshots/screenshot2.png) 134 | 135 | ## Thanks 136 | 137 | - The project originally came from [Quinny898/PersistentSearch](https://github.com/Quinny898/PersistentSearch). 138 | - [Ozodrukh/CircularReveal](https://github.com/ozodrukh/CircularReveal) for reveal animation. 139 | - [kee23](https://github.com/kee23) 140 | 141 | ## License 142 | 143 | Copyright 2015 Cryse Hillmes 144 | 145 | Licensed under the Apache License, Version 2.0 (the "License"); 146 | you may not use this file except in compliance with the License. 147 | You may obtain a copy of the License at 148 | 149 | http://www.apache.org/licenses/LICENSE-2.0 150 | 151 | Unless required by applicable law or agreed to in writing, software 152 | distributed under the License is distributed on an "AS IS" BASIS, 153 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 154 | See the License for the specific language governing permissions and 155 | limitations under the License. 156 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.5.0' 9 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.2' 10 | classpath 'com.github.dcendents:android-maven-gradle-plugin:1.3' 11 | 12 | // NOTE: Do not place your application dependencies here; they belong 13 | // in the individual module build.gradle files 14 | } 15 | } 16 | afterEvaluate { 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | jcenter() 22 | } 23 | 24 | ext.appVersionCode = 7 25 | ext.appVersionName = "1.0.4" 26 | ext.appMinSdkVersion = 14 27 | ext.appTargetSdkVersion = 23 28 | ext.appCompileSdkVersion = 23 29 | ext.appBuildToolsVersion = '23.0.2' 30 | 31 | 32 | ext.supportlibraryVersion = '23.2.1' 33 | } 34 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | org.gradle.parallel=true 19 | org.gradle.daemon=true 20 | org.gradle.configureondemand=true 21 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Apr 10 15:27:10 PDT 2013 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /persistentsearchview/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /persistentsearchview/bintray.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.jfrog.bintray' 2 | 3 | version = libraryVersion 4 | 5 | task sourcesJar(type: Jar) { 6 | from android.sourceSets.main.java.srcDirs 7 | classifier = 'sources' 8 | } 9 | 10 | task javadoc(type: Javadoc) { 11 | source = android.sourceSets.main.java.srcDirs 12 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 13 | destinationDir = file("../javadoc/") 14 | failOnError false 15 | } 16 | 17 | task javadocJar(type: Jar, dependsOn: javadoc) { 18 | classifier = 'javadoc' 19 | from javadoc.destinationDir 20 | } 21 | artifacts { 22 | archives javadocJar 23 | archives sourcesJar 24 | } 25 | 26 | // Bintray 27 | Properties properties = new Properties() 28 | properties.load(project.rootProject.file('local.properties').newDataInputStream()) 29 | 30 | bintray { 31 | user = properties.getProperty("bintray.user") 32 | key = properties.getProperty("bintray.apikey") 33 | 34 | configurations = ['archives'] 35 | pkg { 36 | repo = bintrayRepo 37 | name = bintrayName 38 | desc = libraryDescription 39 | websiteUrl = siteUrl 40 | vcsUrl = gitUrl 41 | licenses = allLicenses 42 | publish = true 43 | publicDownloadNumbers = true 44 | version { 45 | desc = libraryDescription 46 | gpg { 47 | sign = true //Determines whether to GPG sign the files. The default is false 48 | passphrase = properties.getProperty("bintray.gpg.password") 49 | //Optional. The passphrase for GPG signing' 50 | } 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /persistentsearchview/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion appCompileSdkVersion 5 | buildToolsVersion appBuildToolsVersion 6 | 7 | defaultConfig { 8 | minSdkVersion appMinSdkVersion 9 | targetSdkVersion appTargetSdkVersion 10 | versionCode appVersionCode 11 | versionName appVersionName 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | lintOptions { 20 | abortOnError false 21 | } 22 | 23 | } 24 | 25 | dependencies { 26 | compile fileTree(dir: 'libs', include: ['*.jar']) 27 | compile "com.android.support:appcompat-v7:$supportlibraryVersion" 28 | compile "com.android.support:cardview-v7:$supportlibraryVersion" 29 | } 30 | 31 | 32 | 33 | ext { 34 | bintrayRepo = 'maven' 35 | bintrayName = 'persistent-searchview' 36 | 37 | publishedGroupId = 'org.cryse.widget' 38 | libraryName = 'PersistentSearchView' 39 | artifact = 'persistentsearchview' 40 | 41 | libraryDescription = 'A library that implements Google Play like PersistentSearch view.' 42 | 43 | siteUrl = 'https://github.com/crysehillmes/PersistentSearchView' 44 | gitUrl = 'https://github.com/crysehillmes/PersistentSearchView.git' 45 | 46 | libraryVersion = appVersionName 47 | 48 | developerId = 'tyk5555' 49 | developerName = 'Cryse Hillmes' 50 | developerEmail = 'tikey0328@gmail.com' 51 | 52 | licenseName = 'The Apache Software License, Version 2.0' 53 | licenseUrl = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 54 | allLicenses = ["Apache-2.0"] 55 | } 56 | 57 | apply from: 'install.gradle' 58 | apply from: 'bintray.gradle' -------------------------------------------------------------------------------- /persistentsearchview/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Persistent Search 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /persistentsearchview/install.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.github.dcendents.android-maven' 2 | 3 | group = publishedGroupId // Maven Group ID for the artifact 4 | 5 | install { 6 | repositories.mavenInstaller { 7 | // This generates POM.xml with proper parameters 8 | pom { 9 | project { 10 | packaging 'aar' 11 | groupId publishedGroupId 12 | artifactId artifactId 13 | 14 | // Add your description here 15 | name libraryName 16 | description libraryDescription 17 | url siteUrl 18 | 19 | // Set your license 20 | licenses { 21 | license { 22 | name licenseName 23 | url licenseUrl 24 | } 25 | } 26 | developers { 27 | developer { 28 | id developerId 29 | name developerName 30 | email developerEmail 31 | } 32 | } 33 | scm { 34 | connection gitUrl 35 | developerConnection gitUrl 36 | url siteUrl 37 | 38 | } 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /persistentsearchview/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/Kieron/Android/adt-bundle-windows-x86_64-20140702/adt-bundle-windows-x86_64-20140702/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /persistentsearchview/src/androidTest/java/org/cryse/widget/persistentsearch/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/android/support/v7/graphics/drawable/SupportDrawerArrowDrawable.java: -------------------------------------------------------------------------------- 1 | package android.support.v7.graphics.drawable; 2 | 3 | import android.content.Context; 4 | 5 | public class SupportDrawerArrowDrawable extends DrawerArrowDrawable { 6 | 7 | public SupportDrawerArrowDrawable(Context themedContext) { 8 | super(themedContext); 9 | } 10 | 11 | public void setPosition(float position) { 12 | if (position == 1f) { 13 | setVerticalMirror(true); 14 | } else if (position == 0f) { 15 | setVerticalMirror(false); 16 | } 17 | super.setProgress(position); 18 | } 19 | 20 | public float getPosition() { 21 | return super.getProgress(); 22 | } 23 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/animation/RevealAnimator.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.animation.Animator; 4 | import android.annotation.TargetApi; 5 | import android.graphics.Rect; 6 | import android.os.Build; 7 | import android.util.Property; 8 | import android.view.View; 9 | 10 | import java.lang.ref.WeakReference; 11 | 12 | import static io.codetail.animation.ViewAnimationUtils.SimpleAnimationListener; 13 | 14 | /** 15 | * @hide 16 | */ 17 | public interface RevealAnimator{ 18 | 19 | RevealRadius CLIP_RADIUS = new RevealRadius(); 20 | 21 | /** 22 | * Listen when animation start/end/cancel 23 | * and setup view for it 24 | */ 25 | void onRevealAnimationStart(); 26 | void onRevealAnimationEnd(); 27 | void onRevealAnimationCancel(); 28 | 29 | /** 30 | * Used with animator to animate view clipping 31 | * 32 | * @param value clip radius 33 | */ 34 | void setRevealRadius(float value); 35 | 36 | /** 37 | * Used with animator to animate view clipping 38 | * 39 | * @return current radius 40 | */ 41 | float getRevealRadius(); 42 | 43 | /** 44 | * Invalidate certain rectangle 45 | * 46 | * @param bounds bounds to redraw 47 | * @see View#invalidate(Rect) 48 | */ 49 | void invalidate(Rect bounds); 50 | 51 | /** 52 | * {@link ViewAnimationUtils#createCircularReveal(View, int, int, float, float)} is 53 | * called it creates new {@link io.codetail.animation.RevealAnimator.RevealInfo} 54 | * and attaches to parent, here is necessary data about animation 55 | * 56 | * @param info reveal information 57 | * 58 | * @see RevealAnimator.RevealInfo 59 | */ 60 | void attachRevealInfo(RevealInfo info); 61 | 62 | /** 63 | * Returns new {@link SupportAnimator} that plays 64 | * reversed animation of current one 65 | * 66 | * This method might be temporary, you should call 67 | * {@link SupportAnimator#reverse()} instead 68 | * 69 | * @hide 70 | * @return reverse {@link SupportAnimator} 71 | * 72 | * @see SupportAnimator#reverse() 73 | */ 74 | SupportAnimator startReverseAnimation(); 75 | 76 | class RevealInfo{ 77 | public final int centerX; 78 | public final int centerY; 79 | public final float startRadius; 80 | public final float endRadius; 81 | public final WeakReference target; 82 | 83 | public RevealInfo(int centerX, int centerY, float startRadius, float endRadius, 84 | WeakReference target) { 85 | this.centerX = centerX; 86 | this.centerY = centerY; 87 | this.startRadius = startRadius; 88 | this.endRadius = endRadius; 89 | this.target = target; 90 | } 91 | 92 | public View getTarget(){ 93 | return target.get(); 94 | } 95 | 96 | public boolean hasTarget(){ 97 | return getTarget() != null; 98 | } 99 | } 100 | 101 | class RevealFinishedGingerbread extends SimpleAnimationListener { 102 | WeakReference mReference; 103 | 104 | RevealFinishedGingerbread(RevealAnimator target) { 105 | mReference = new WeakReference<>(target); 106 | } 107 | 108 | @Override 109 | public void onAnimationStart(Animator animation) { 110 | RevealAnimator target = mReference.get(); 111 | target.onRevealAnimationStart(); 112 | } 113 | 114 | @Override 115 | public void onAnimationCancel(Animator animation) { 116 | RevealAnimator target = mReference.get(); 117 | target.onRevealAnimationCancel(); 118 | } 119 | 120 | @Override 121 | public void onAnimationEnd(Animator animation) { 122 | RevealAnimator target = mReference.get(); 123 | target.onRevealAnimationEnd(); 124 | } 125 | } 126 | 127 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 128 | class RevealFinishedIceCreamSandwich extends RevealFinishedGingerbread { 129 | int mFeaturedLayerType; 130 | int mLayerType; 131 | 132 | RevealFinishedIceCreamSandwich(RevealAnimator target) { 133 | super(target); 134 | 135 | mLayerType = ((View) target).getLayerType(); 136 | mFeaturedLayerType = View.LAYER_TYPE_SOFTWARE; 137 | } 138 | 139 | @Override 140 | public void onAnimationCancel(Animator animation) { 141 | ((View) mReference.get()).setLayerType(mLayerType, null); 142 | super.onAnimationEnd(animation); 143 | } 144 | 145 | @Override 146 | public void onAnimationStart(Animator animation) { 147 | ((View) mReference.get()).setLayerType(mFeaturedLayerType, null); 148 | super.onAnimationStart(animation); 149 | } 150 | 151 | @Override 152 | public void onAnimationEnd(Animator animation) { 153 | ((View) mReference.get()).setLayerType(mLayerType, null); 154 | super.onAnimationEnd(animation); 155 | } 156 | } 157 | 158 | class RevealFinishedJellyBeanMr2 extends RevealFinishedIceCreamSandwich { 159 | 160 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 161 | RevealFinishedJellyBeanMr2(RevealAnimator target) { 162 | super(target); 163 | 164 | mFeaturedLayerType = View.LAYER_TYPE_HARDWARE; 165 | } 166 | } 167 | 168 | class RevealRadius extends Property { 169 | 170 | public RevealRadius() { 171 | super(Float.TYPE, "revealRadius"); 172 | } 173 | 174 | @Override 175 | public void set(RevealAnimator object, Float value) { 176 | object.setRevealRadius(value); 177 | } 178 | 179 | @Override 180 | public Float get(RevealAnimator object) { 181 | return object.getRevealRadius(); 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/animation/SupportAnimator.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.view.animation.Interpolator; 4 | 5 | import java.lang.ref.WeakReference; 6 | 7 | public abstract class SupportAnimator { 8 | 9 | WeakReference mTarget; 10 | 11 | public SupportAnimator(RevealAnimator target) { 12 | mTarget = new WeakReference<>(target); 13 | } 14 | 15 | /** 16 | * @return true if using native android animation framework, otherwise is 17 | * nineoldandroids 18 | */ 19 | public abstract boolean isNativeAnimator(); 20 | 21 | /** 22 | * @return depends from {@link android.os.Build.VERSION} if sdk version 23 | * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and greater will return 24 | * {@link android.animation.Animator} 25 | */ 26 | public abstract Object get(); 27 | 28 | /** 29 | * Starts this animation. If the animation has a nonzero startDelay, the animation will start 30 | * running after that delay elapses. A non-delayed animation will have its initial 31 | * value(s) set immediately, followed by calls to 32 | * {@link android.animation.Animator.AnimatorListener#onAnimationStart(android.animation.Animator)} 33 | * for any listeners of this animator. 34 | * 35 | *

The animation started by calling this method will be run on the thread that called 36 | * this method. This thread should have a Looper on it (a runtime exception will be thrown if 37 | * this is not the case). Also, if the animation will animate 38 | * properties of objects in the view hierarchy, then the calling thread should be the UI 39 | * thread for that view hierarchy.

40 | * 41 | */ 42 | public abstract void start(); 43 | 44 | /** 45 | * Sets the duration of the animation. 46 | * 47 | * @param duration The length of the animation, in milliseconds. 48 | */ 49 | public abstract void setDuration(int duration); 50 | 51 | /** 52 | * The time interpolator used in calculating the elapsed fraction of the 53 | * animation. The interpolator determines whether the animation runs with 54 | * linear or non-linear motion, such as acceleration and deceleration. The 55 | * default value is {@link android.view.animation.AccelerateDecelerateInterpolator}. 56 | * 57 | * @param value the interpolator to be used by this animation 58 | */ 59 | public abstract void setInterpolator(Interpolator value); 60 | 61 | 62 | /** 63 | * Adds a listener to the set of listeners that are sent events through the life of an 64 | * animation, such as start, repeat, and end. 65 | * 66 | * @param listener the listener to be added to the current set of listeners for this animation. 67 | */ 68 | public abstract void addListener(AnimatorListener listener); 69 | 70 | 71 | /** 72 | * Returns whether this Animator is currently running (having been started and gone past any 73 | * initial startDelay period and not yet ended). 74 | * 75 | * @return Whether the Animator is running. 76 | */ 77 | public abstract boolean isRunning(); 78 | 79 | 80 | /** 81 | * Cancels the animation. Unlike {@link #end()}, cancel() causes the animation to 82 | * stop in its tracks, sending an 83 | * {@link AnimatorListener#onAnimationCancel()} to 84 | * its listeners, followed by an 85 | * {@link AnimatorListener#onAnimationEnd()} message. 86 | * 87 | *

This method must be called on the thread that is running the animation.

88 | */ 89 | public abstract void cancel(); 90 | 91 | /** 92 | * Ends the animation. This causes the animation to assign the end value of the property being 93 | * animated, then calling the 94 | * {@link AnimatorListener#onAnimationEnd()} method on 95 | * its listeners. 96 | * 97 | *

This method must be called on the thread that is running the animation.

98 | */ 99 | public void end() { 100 | } 101 | 102 | /** 103 | * This method tells the object to use appropriate information to extract 104 | * starting values for the animation. For example, a AnimatorSet object will pass 105 | * this call to its child objects to tell them to set up the values. A 106 | * ObjectAnimator object will use the information it has about its target object 107 | * and PropertyValuesHolder objects to get the start values for its properties. 108 | * A ValueAnimator object will ignore the request since it does not have enough 109 | * information (such as a target object) to gather these values. 110 | */ 111 | public void setupStartValues() { 112 | } 113 | 114 | /** 115 | * This method tells the object to use appropriate information to extract 116 | * ending values for the animation. For example, a AnimatorSet object will pass 117 | * this call to its child objects to tell them to set up the values. A 118 | * ObjectAnimator object will use the information it has about its target object 119 | * and PropertyValuesHolder objects to get the start values for its properties. 120 | * A ValueAnimator object will ignore the request since it does not have enough 121 | * information (such as a target object) to gather these values. 122 | */ 123 | public void setupEndValues() { 124 | } 125 | 126 | /** 127 | * Experimental feature 128 | */ 129 | public SupportAnimator reverse() { 130 | if(isRunning()){ 131 | return null; 132 | } 133 | 134 | RevealAnimator target = mTarget.get(); 135 | if(target != null){ 136 | return target.startReverseAnimation(); 137 | } 138 | 139 | return null; 140 | } 141 | 142 | /** 143 | *

An animation listener receives notifications from an animation. 144 | * Notifications indicate animation related events, such as the end or the 145 | * repetition of the animation.

146 | */ 147 | public interface AnimatorListener { 148 | /** 149 | *

Notifies the start of the animation.

150 | */ 151 | void onAnimationStart(); 152 | 153 | /** 154 | *

Notifies the end of the animation. This callback is not invoked 155 | * for animations with repeat count set to INFINITE.

156 | */ 157 | void onAnimationEnd(); 158 | 159 | /** 160 | *

Notifies the cancellation of the animation. This callback is not invoked 161 | * for animations with repeat count set to INFINITE.

162 | */ 163 | void onAnimationCancel(); 164 | 165 | /** 166 | *

Notifies the repetition of the animation.

167 | */ 168 | void onAnimationRepeat(); 169 | } 170 | 171 | /** 172 | *

Provides default implementation for AnimatorListener.

173 | */ 174 | public static abstract class SimpleAnimatorListener implements AnimatorListener { 175 | 176 | @Override 177 | public void onAnimationStart() { 178 | 179 | } 180 | 181 | @Override 182 | public void onAnimationEnd() { 183 | 184 | } 185 | 186 | @Override 187 | public void onAnimationCancel() { 188 | 189 | } 190 | 191 | @Override 192 | public void onAnimationRepeat() { 193 | 194 | } 195 | } 196 | 197 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/animation/SupportAnimatorLollipop.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.animation.Animator; 4 | import android.annotation.TargetApi; 5 | import android.os.Build; 6 | import android.view.animation.Interpolator; 7 | 8 | import java.lang.ref.WeakReference; 9 | 10 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 11 | final class SupportAnimatorLollipop extends SupportAnimator{ 12 | 13 | WeakReference mAnimator; 14 | 15 | SupportAnimatorLollipop(Animator animator, RevealAnimator target) { 16 | super(target); 17 | mAnimator = new WeakReference<>(animator); 18 | } 19 | 20 | @Override 21 | public boolean isNativeAnimator() { 22 | return true; 23 | } 24 | 25 | @Override 26 | public Object get() { 27 | return mAnimator.get(); 28 | } 29 | 30 | 31 | @Override 32 | public void start() { 33 | Animator a = mAnimator.get(); 34 | if(a != null) { 35 | a.start(); 36 | } 37 | } 38 | 39 | @Override 40 | public void setDuration(int duration) { 41 | Animator a = mAnimator.get(); 42 | if(a != null) { 43 | a.setDuration(duration); 44 | } 45 | } 46 | 47 | @Override 48 | public void setInterpolator(Interpolator value) { 49 | Animator a = mAnimator.get(); 50 | if(a != null) { 51 | a.setInterpolator(value); 52 | } 53 | } 54 | 55 | @Override 56 | public void addListener(final AnimatorListener listener) { 57 | Animator a = mAnimator.get(); 58 | if(a == null) { 59 | return; 60 | } 61 | 62 | if(listener == null){ 63 | a.addListener(null); 64 | return; 65 | } 66 | 67 | a.addListener(new Animator.AnimatorListener() { 68 | @Override 69 | public void onAnimationStart(Animator animation) { 70 | listener.onAnimationStart(); 71 | } 72 | 73 | @Override 74 | public void onAnimationEnd(Animator animation) { 75 | listener.onAnimationEnd(); 76 | } 77 | 78 | @Override 79 | public void onAnimationCancel(Animator animation) { 80 | listener.onAnimationCancel(); 81 | } 82 | 83 | @Override 84 | public void onAnimationRepeat(Animator animation) { 85 | listener.onAnimationRepeat(); 86 | } 87 | }); 88 | } 89 | 90 | @Override 91 | public boolean isRunning() { 92 | Animator a = mAnimator.get(); 93 | return a != null && a.isRunning(); 94 | } 95 | 96 | @Override 97 | public void cancel() { 98 | Animator a = mAnimator.get(); 99 | if(a != null){ 100 | a.cancel(); 101 | } 102 | } 103 | 104 | @Override 105 | public void end() { 106 | Animator a = mAnimator.get(); 107 | if(a != null){ 108 | a.end(); 109 | } 110 | } 111 | 112 | @Override 113 | public void setupStartValues() { 114 | Animator a = mAnimator.get(); 115 | if(a != null){ 116 | a.setupStartValues(); 117 | } 118 | } 119 | 120 | @Override 121 | public void setupEndValues() { 122 | Animator a = mAnimator.get(); 123 | if(a != null){ 124 | a.setupEndValues(); 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/animation/SupportAnimatorPreL.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.animation.Animator; 4 | import android.view.animation.Interpolator; 5 | 6 | import java.lang.ref.WeakReference; 7 | 8 | final class SupportAnimatorPreL extends SupportAnimator { 9 | 10 | WeakReference mAnimator; 11 | 12 | SupportAnimatorPreL(Animator animator, RevealAnimator target) { 13 | super(target); 14 | mAnimator = new WeakReference<>(animator); 15 | } 16 | 17 | @Override 18 | public boolean isNativeAnimator() { 19 | return false; 20 | } 21 | 22 | @Override 23 | public Object get() { 24 | return mAnimator.get(); 25 | } 26 | 27 | @Override 28 | public void start() { 29 | Animator a = mAnimator.get(); 30 | if(a != null) { 31 | a.start(); 32 | } 33 | } 34 | 35 | @Override 36 | public void setDuration(int duration) { 37 | Animator a = mAnimator.get(); 38 | if(a != null) { 39 | a.setDuration(duration); 40 | } 41 | } 42 | 43 | @Override 44 | public void setInterpolator(Interpolator value) { 45 | Animator a = mAnimator.get(); 46 | if(a != null) { 47 | a.setInterpolator(value); 48 | } 49 | } 50 | 51 | @Override 52 | public void addListener(final AnimatorListener listener) { 53 | Animator a = mAnimator.get(); 54 | if(a == null) { 55 | return; 56 | } 57 | 58 | if(listener == null){ 59 | a.addListener(null); 60 | return; 61 | } 62 | 63 | a.addListener(new Animator.AnimatorListener() { 64 | @Override 65 | public void onAnimationStart(Animator animation) { 66 | listener.onAnimationStart(); 67 | } 68 | 69 | @Override 70 | public void onAnimationEnd(Animator animation) { 71 | listener.onAnimationEnd(); 72 | } 73 | 74 | @Override 75 | public void onAnimationCancel(Animator animation) { 76 | listener.onAnimationCancel(); 77 | } 78 | 79 | @Override 80 | public void onAnimationRepeat(Animator animation) { 81 | listener.onAnimationRepeat(); 82 | } 83 | }); 84 | } 85 | 86 | @Override 87 | public boolean isRunning() { 88 | Animator a = mAnimator.get(); 89 | return a != null && a.isRunning(); 90 | } 91 | 92 | @Override 93 | public void cancel() { 94 | Animator a = mAnimator.get(); 95 | if(a != null){ 96 | a.cancel(); 97 | } 98 | } 99 | 100 | @Override 101 | public void end() { 102 | Animator a = mAnimator.get(); 103 | if(a != null){ 104 | a.end(); 105 | } 106 | } 107 | 108 | @Override 109 | public void setupStartValues() { 110 | Animator a = mAnimator.get(); 111 | if(a != null){ 112 | a.setupStartValues(); 113 | } 114 | } 115 | 116 | @Override 117 | public void setupEndValues() { 118 | Animator a = mAnimator.get(); 119 | if(a != null){ 120 | a.setupEndValues(); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/animation/ViewAnimationUtils.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.animation.Animator; 4 | import android.animation.ObjectAnimator; 5 | import android.annotation.TargetApi; 6 | import android.os.Build; 7 | import android.view.View; 8 | import android.view.animation.AccelerateDecelerateInterpolator; 9 | 10 | import java.lang.ref.WeakReference; 11 | 12 | import io.codetail.animation.RevealAnimator.RevealInfo; 13 | 14 | import static android.os.Build.VERSION.SDK_INT; 15 | import static android.os.Build.VERSION_CODES.LOLLIPOP; 16 | import static io.codetail.animation.RevealAnimator.CLIP_RADIUS; 17 | 18 | public class ViewAnimationUtils { 19 | 20 | private final static boolean LOLLIPOP_PLUS = SDK_INT >= LOLLIPOP; 21 | 22 | public static final int SCALE_UP_DURATION = 500; 23 | 24 | /** 25 | * Returns an Animator which can animate a clipping circle. 26 | *

27 | * Any shadow cast by the View will respect the circular clip from this animator. 28 | *

29 | * Only a single non-rectangular clip can be applied on a View at any time. 30 | * Views clipped by a circular reveal animation take priority over 31 | * {@link android.view.View#setClipToOutline(boolean) View Outline clipping}. 32 | *

33 | * Note that the animation returned here is a one-shot animation. It cannot 34 | * be re-used, and once started it cannot be paused or resumed. 35 | * 36 | * @param view The View will be clipped to the animating circle. 37 | * @param centerX The x coordinate of the center of the animating circle. 38 | * @param centerY The y coordinate of the center of the animating circle. 39 | * @param startRadius The starting radius of the animating circle. 40 | * @param endRadius The ending radius of the animating circle. 41 | */ 42 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 43 | public static SupportAnimator createCircularReveal(View view, 44 | int centerX, int centerY, 45 | float startRadius, float endRadius) { 46 | 47 | if(!(view.getParent() instanceof RevealAnimator)){ 48 | throw new IllegalArgumentException("View must be inside RevealFrameLayout or RevealLinearLayout."); 49 | } 50 | 51 | RevealAnimator revealLayout = (RevealAnimator) view.getParent(); 52 | revealLayout.attachRevealInfo(new RevealInfo(centerX, centerY, startRadius, endRadius, 53 | new WeakReference<>(view))); 54 | 55 | if(LOLLIPOP_PLUS){ 56 | return new SupportAnimatorLollipop(android.view.ViewAnimationUtils 57 | .createCircularReveal(view, centerX, centerY, startRadius, endRadius), revealLayout); 58 | } 59 | 60 | ObjectAnimator reveal = ObjectAnimator.ofFloat(revealLayout, CLIP_RADIUS, 61 | startRadius, endRadius); 62 | reveal.addListener(getRevealFinishListener(revealLayout)); 63 | 64 | return new SupportAnimatorPreL(reveal, revealLayout); 65 | } 66 | 67 | private static Animator.AnimatorListener getRevealFinishListener(RevealAnimator target){ 68 | if(SDK_INT >= 18){ 69 | return new RevealAnimator.RevealFinishedJellyBeanMr2(target); 70 | }else if(SDK_INT >= 14){ 71 | return new RevealAnimator.RevealFinishedIceCreamSandwich(target); 72 | }else { 73 | return new RevealAnimator.RevealFinishedGingerbread(target); 74 | } 75 | } 76 | 77 | /** 78 | * Lifting view 79 | * 80 | * @param view The animation target 81 | * @param baseRotation initial Rotation X in 3D space 82 | * @param fromY initial Y position of view 83 | * @param duration aniamtion duration 84 | * @param startDelay start delay before animation begin 85 | */ 86 | @Deprecated 87 | public static void liftingFromBottom(View view, float baseRotation, float fromY, int duration, int startDelay){ 88 | view.setRotationX(baseRotation); 89 | view.setTranslationY(fromY); 90 | 91 | view 92 | .animate() 93 | .setInterpolator(new AccelerateDecelerateInterpolator()) 94 | .setDuration(duration) 95 | .setStartDelay(startDelay) 96 | .rotationX(0) 97 | .translationY(0) 98 | .start(); 99 | 100 | } 101 | 102 | /** 103 | * Lifting view 104 | * 105 | * @param view The animation target 106 | * @param baseRotation initial Rotation X in 3D space 107 | * @param duration aniamtion duration 108 | * @param startDelay start delay before animation begin 109 | */ 110 | @Deprecated 111 | public static void liftingFromBottom(View view, float baseRotation, int duration, int startDelay){ 112 | view.setRotationX(baseRotation); 113 | view.setTranslationY(view.getHeight() / 3); 114 | 115 | view 116 | .animate() 117 | .setInterpolator(new AccelerateDecelerateInterpolator()) 118 | .setDuration(duration) 119 | .setStartDelay(startDelay) 120 | .rotationX(0) 121 | .translationY(0) 122 | .start(); 123 | 124 | } 125 | 126 | /** 127 | * Lifting view 128 | * 129 | * @param view The animation target 130 | * @param baseRotation initial Rotation X in 3D space 131 | * @param duration aniamtion duration 132 | */ 133 | @Deprecated 134 | public static void liftingFromBottom(View view, float baseRotation, int duration){ 135 | view.setRotationX(baseRotation); 136 | view.setTranslationY(view.getHeight() / 3); 137 | 138 | view 139 | .animate() 140 | .setInterpolator(new AccelerateDecelerateInterpolator()) 141 | .setDuration(duration) 142 | .rotationX(0) 143 | .translationY(0) 144 | .start(); 145 | 146 | } 147 | 148 | static class SimpleAnimationListener implements Animator.AnimatorListener{ 149 | 150 | @Override 151 | public void onAnimationStart(Animator animation) { 152 | 153 | } 154 | 155 | @Override 156 | public void onAnimationEnd(Animator animation) { 157 | 158 | } 159 | 160 | @Override 161 | public void onAnimationCancel(Animator animation) { 162 | 163 | } 164 | 165 | @Override 166 | public void onAnimationRepeat(Animator animation) { 167 | 168 | } 169 | } 170 | 171 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/widget/RevealFrameLayout.java: -------------------------------------------------------------------------------- 1 | package io.codetail.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.graphics.Rect; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.widget.FrameLayout; 10 | 11 | import io.codetail.animation.RevealAnimator; 12 | import io.codetail.animation.SupportAnimator; 13 | import io.codetail.animation.ViewAnimationUtils; 14 | 15 | public class RevealFrameLayout extends FrameLayout implements RevealAnimator{ 16 | 17 | private Path mRevealPath; 18 | private final Rect mTargetBounds = new Rect(); 19 | private RevealInfo mRevealInfo; 20 | private boolean mRunning; 21 | private float mRadius; 22 | 23 | public RevealFrameLayout(Context context) { 24 | this(context, null); 25 | } 26 | 27 | public RevealFrameLayout(Context context, AttributeSet attrs) { 28 | this(context, attrs, 0); 29 | } 30 | 31 | public RevealFrameLayout(Context context, AttributeSet attrs, int defStyle) { 32 | super(context, attrs, defStyle); 33 | mRevealPath = new Path(); 34 | } 35 | 36 | @Override 37 | public void onRevealAnimationStart() { 38 | mRunning = true; 39 | } 40 | 41 | @Override 42 | public void onRevealAnimationEnd() { 43 | mRunning = false; 44 | invalidate(mTargetBounds); 45 | } 46 | 47 | @Override 48 | public void onRevealAnimationCancel() { 49 | onRevealAnimationEnd(); 50 | } 51 | 52 | /** 53 | * Circle radius size 54 | * 55 | * @hide 56 | */ 57 | @Override 58 | public void setRevealRadius(float radius){ 59 | mRadius = radius; 60 | mRevealInfo.getTarget().getHitRect(mTargetBounds); 61 | invalidate(mTargetBounds); 62 | } 63 | 64 | /** 65 | * Circle radius size 66 | * 67 | * @hide 68 | */ 69 | @Override 70 | public float getRevealRadius(){ 71 | return mRadius; 72 | } 73 | 74 | /** 75 | * @hide 76 | */ 77 | @Override 78 | public void attachRevealInfo(RevealInfo info) { 79 | mRevealInfo = info; 80 | } 81 | 82 | /** 83 | * @hide 84 | */ 85 | @Override 86 | public SupportAnimator startReverseAnimation() { 87 | if(mRevealInfo != null && mRevealInfo.hasTarget() && !mRunning) { 88 | return ViewAnimationUtils.createCircularReveal(mRevealInfo.getTarget(), 89 | mRevealInfo.centerX, mRevealInfo.centerY, 90 | mRevealInfo.endRadius, mRevealInfo.startRadius); 91 | } 92 | return null; 93 | } 94 | 95 | @Override 96 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 97 | if(mRunning && child == mRevealInfo.getTarget()){ 98 | final int state = canvas.save(); 99 | 100 | mRevealPath.reset(); 101 | mRevealPath.addCircle(mRevealInfo.centerX, mRevealInfo.centerY, mRadius, Path.Direction.CW); 102 | 103 | canvas.clipPath(mRevealPath); 104 | 105 | boolean isInvalided = super.drawChild(canvas, child, drawingTime); 106 | 107 | canvas.restoreToCount(state); 108 | 109 | return isInvalided; 110 | } 111 | 112 | return super.drawChild(canvas, child, drawingTime); 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/io/codetail/widget/RevealLinearLayout.java: -------------------------------------------------------------------------------- 1 | package io.codetail.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.graphics.Rect; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.widget.LinearLayout; 10 | 11 | import io.codetail.animation.RevealAnimator; 12 | import io.codetail.animation.SupportAnimator; 13 | import io.codetail.animation.ViewAnimationUtils; 14 | 15 | public class RevealLinearLayout extends LinearLayout implements RevealAnimator{ 16 | 17 | private Path mRevealPath; 18 | private final Rect mTargetBounds = new Rect(); 19 | private RevealInfo mRevealInfo; 20 | private boolean mRunning; 21 | private float mRadius; 22 | 23 | public RevealLinearLayout(Context context) { 24 | this(context, null); 25 | } 26 | 27 | public RevealLinearLayout(Context context, AttributeSet attrs) { 28 | this(context, attrs, 0); 29 | } 30 | 31 | public RevealLinearLayout(Context context, AttributeSet attrs, int defStyle) { 32 | super(context, attrs); 33 | mRevealPath = new Path(); 34 | } 35 | 36 | @Override 37 | public void onRevealAnimationStart() { 38 | mRunning = true; 39 | } 40 | 41 | @Override 42 | public void onRevealAnimationEnd() { 43 | mRunning = false; 44 | invalidate(mTargetBounds); 45 | } 46 | 47 | @Override 48 | public void onRevealAnimationCancel() { 49 | onRevealAnimationEnd(); 50 | } 51 | 52 | /** 53 | * Circle radius size 54 | * 55 | * @hide 56 | */ 57 | @Override 58 | public void setRevealRadius(float radius){ 59 | mRadius = radius; 60 | mRevealInfo.getTarget().getHitRect(mTargetBounds); 61 | invalidate(mTargetBounds); 62 | } 63 | 64 | /** 65 | * Circle radius size 66 | * 67 | * @hide 68 | */ 69 | @Override 70 | public float getRevealRadius(){ 71 | return mRadius; 72 | } 73 | 74 | /** 75 | * @hide 76 | */ 77 | @Override 78 | public void attachRevealInfo(RevealInfo info) { 79 | mRevealInfo = info; 80 | } 81 | 82 | /** 83 | * @hide 84 | */ 85 | @Override 86 | public SupportAnimator startReverseAnimation() { 87 | if(mRevealInfo != null && mRevealInfo.hasTarget() && !mRunning) { 88 | return ViewAnimationUtils.createCircularReveal(mRevealInfo.getTarget(), 89 | mRevealInfo.centerX, mRevealInfo.centerY, 90 | mRevealInfo.endRadius, mRevealInfo.startRadius); 91 | } 92 | return null; 93 | } 94 | 95 | @Override 96 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 97 | if(mRunning && child == mRevealInfo.getTarget()){ 98 | final int state = canvas.save(); 99 | 100 | mRevealPath.reset(); 101 | mRevealPath.addCircle(mRevealInfo.centerX, mRevealInfo.centerY, mRadius, Path.Direction.CW); 102 | 103 | canvas.clipPath(mRevealPath); 104 | 105 | boolean isInvalided = super.drawChild(canvas, child, drawingTime); 106 | 107 | canvas.restoreToCount(state); 108 | 109 | return isInvalided; 110 | } 111 | 112 | return super.drawChild(canvas, child, drawingTime); 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/DefaultVoiceRecognizerDelegate.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.Intent; 6 | import android.content.pm.PackageManager; 7 | import android.content.pm.ResolveInfo; 8 | import android.speech.RecognizerIntent; 9 | 10 | import java.util.List; 11 | 12 | public class DefaultVoiceRecognizerDelegate extends VoiceRecognitionDelegate { 13 | 14 | public DefaultVoiceRecognizerDelegate(Activity activity) { 15 | super(activity); 16 | } 17 | 18 | public DefaultVoiceRecognizerDelegate(Activity activity, int activityRequestCode) { 19 | super(activity, activityRequestCode); 20 | } 21 | 22 | public DefaultVoiceRecognizerDelegate(Fragment fragment) { 23 | super(fragment); 24 | } 25 | 26 | public DefaultVoiceRecognizerDelegate(Fragment fragment, int activityRequestCode) { 27 | super(fragment, activityRequestCode); 28 | } 29 | 30 | public DefaultVoiceRecognizerDelegate(android.support.v4.app.Fragment supportFragment) { 31 | super(supportFragment); 32 | } 33 | 34 | public DefaultVoiceRecognizerDelegate(android.support.v4.app.Fragment supportFragment, int activityRequestCode) { 35 | super(supportFragment, activityRequestCode); 36 | } 37 | 38 | @Override 39 | public Intent buildVoiceRecognitionIntent() { 40 | Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 41 | intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 42 | RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); 43 | intent.putExtra(RecognizerIntent.EXTRA_PROMPT, 44 | getContext().getString(R.string.speak_now)); 45 | return intent; 46 | } 47 | 48 | @Override 49 | public boolean isVoiceRecognitionAvailable() { 50 | Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 51 | PackageManager mgr = getContext().getPackageManager(); 52 | if (mgr != null) { 53 | List list = mgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); 54 | return list.size() > 0; 55 | } 56 | return false; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/HomeButton.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.animation.ObjectAnimator; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.graphics.Color; 7 | import android.graphics.ColorFilter; 8 | import android.graphics.PorterDuff; 9 | import android.graphics.PorterDuffColorFilter; 10 | import android.os.Build; 11 | import android.support.v7.graphics.drawable.SupportDrawerArrowDrawable; 12 | import android.util.AttributeSet; 13 | import android.util.Property; 14 | import android.widget.ImageView; 15 | 16 | public class HomeButton extends ImageView { 17 | public enum IconState { 18 | BURGER, ARROW; 19 | public int toDrawablePosition() { 20 | switch (this) { 21 | case BURGER: 22 | return 0; 23 | case ARROW: 24 | return 1; 25 | default: 26 | return 0; 27 | } 28 | } 29 | } 30 | private ArrowDrawablePositionProperty mArrowPositionProperty = new ArrowDrawablePositionProperty(); 31 | private SupportDrawerArrowDrawable mArrowDrawable; 32 | 33 | private IconState mButtonState = IconState.BURGER; 34 | private long mAnimationDuration = 300l; 35 | public HomeButton(Context context) { 36 | super(context); 37 | init(); 38 | } 39 | 40 | public HomeButton(Context context, AttributeSet attrs) { 41 | super(context, attrs); 42 | init(); 43 | } 44 | 45 | public HomeButton(Context context, AttributeSet attrs, int defStyleAttr) { 46 | super(context, attrs, defStyleAttr); 47 | init(); 48 | } 49 | 50 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 51 | public HomeButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 52 | super(context, attrs, defStyleAttr, defStyleRes); 53 | init(); 54 | } 55 | 56 | public void setArrowDrawableColor(int color) { 57 | ColorFilter colorFilter = new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN); 58 | mArrowDrawable.setColorFilter(colorFilter); 59 | } 60 | 61 | private void init() { 62 | mArrowDrawable = new SupportDrawerArrowDrawable(getContext()); 63 | ColorFilter colorFilter = new PorterDuffColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); 64 | mArrowDrawable.setColorFilter(colorFilter); 65 | this.setImageDrawable(mArrowDrawable); 66 | } 67 | 68 | public void setState(IconState state) { 69 | mButtonState = state; 70 | mArrowDrawable.setPosition(mButtonState.toDrawablePosition()); 71 | } 72 | 73 | public void animateState(IconState state) { 74 | float to = state.toDrawablePosition(); 75 | float from = mButtonState.toDrawablePosition(); 76 | mButtonState = state; 77 | if(Float.compare(from, to) == 0) { 78 | // mArrowDrawable.setPosition(mButtonState.toDrawablePosition()); 79 | } else { 80 | ObjectAnimator.ofFloat(mArrowDrawable, mArrowPositionProperty, from, to).setDuration(mAnimationDuration).start(); 81 | } 82 | //mArrowDrawable.setPosition(mButtonState.toDrawablePosition()); 83 | } 84 | 85 | public void setAnimationDuration(long animationDuration) { 86 | this.mAnimationDuration = animationDuration; 87 | } 88 | 89 | static class ArrowDrawablePositionProperty extends Property { 90 | 91 | public ArrowDrawablePositionProperty() { 92 | super(Float.TYPE, "position"); 93 | } 94 | 95 | @Override 96 | public void set(SupportDrawerArrowDrawable object, Float value) { 97 | object.setPosition(value); 98 | } 99 | 100 | @Override 101 | public Float get(SupportDrawerArrowDrawable object) { 102 | return object.getPosition(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/LogoView.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.graphics.Canvas; 6 | import android.graphics.drawable.Drawable; 7 | import android.os.Build; 8 | import android.support.annotation.DrawableRes; 9 | import android.support.v4.content.res.ResourcesCompat; 10 | import android.text.TextUtils; 11 | import android.util.AttributeSet; 12 | import android.widget.TextView; 13 | 14 | public class LogoView extends TextView { 15 | private Drawable mLogoDrawable; 16 | public LogoView(Context context) { 17 | super(context); 18 | } 19 | 20 | public LogoView(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | } 23 | 24 | public LogoView(Context context, AttributeSet attrs, int defStyleAttr) { 25 | super(context, attrs, defStyleAttr); 26 | } 27 | 28 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 29 | public LogoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 30 | super(context, attrs, defStyleAttr, defStyleRes); 31 | } 32 | 33 | public void setLogo(String logoText) 34 | { 35 | setLogo(new TextDrawable(getResources(), logoText)); 36 | } 37 | 38 | public void setLogo(@DrawableRes int drawableRes) { 39 | setLogo(ResourcesCompat.getDrawable(getResources(), drawableRes, null)); 40 | } 41 | 42 | public void setLogo(Drawable drawable) { 43 | this.mLogoDrawable = drawable; 44 | mLogoDrawable.setBounds(0,0, mLogoDrawable.getIntrinsicWidth(), mLogoDrawable.getIntrinsicHeight()); 45 | this.invalidate(); 46 | } 47 | 48 | @Override 49 | protected void onDraw(Canvas canvas) { 50 | if(!TextUtils.isEmpty(getText())) 51 | super.onDraw(canvas); 52 | else { 53 | if(mLogoDrawable != null) { 54 | mLogoDrawable.draw(canvas); 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/PersistentSearchView.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.animation.LayoutTransition; 4 | import android.content.Context; 5 | import android.content.res.Resources; 6 | import android.content.res.TypedArray; 7 | import android.graphics.Color; 8 | import android.graphics.drawable.Drawable; 9 | import android.inputmethodservice.KeyboardView; 10 | import android.os.Build; 11 | import android.os.Parcel; 12 | import android.os.Parcelable; 13 | import android.support.v4.content.res.ResourcesCompat; 14 | import android.support.v4.view.ViewCompat; 15 | import android.support.v7.widget.CardView; 16 | import android.text.Editable; 17 | import android.text.Layout; 18 | import android.text.TextUtils; 19 | import android.text.TextWatcher; 20 | import android.util.AttributeSet; 21 | import android.util.DisplayMetrics; 22 | import android.util.SparseArray; 23 | import android.util.TypedValue; 24 | import android.view.KeyEvent; 25 | import android.view.LayoutInflater; 26 | import android.view.MotionEvent; 27 | import android.view.View; 28 | import android.view.ViewTreeObserver; 29 | import android.view.animation.AccelerateDecelerateInterpolator; 30 | import android.view.inputmethod.EditorInfo; 31 | import android.view.inputmethod.InputMethodManager; 32 | import android.widget.AdapterView; 33 | import android.widget.EditText; 34 | import android.widget.FrameLayout; 35 | import android.widget.ImageView; 36 | import android.widget.ListView; 37 | import android.widget.RelativeLayout; 38 | import android.widget.TextView; 39 | 40 | import java.util.ArrayList; 41 | import java.util.Collection; 42 | 43 | import io.codetail.animation.SupportAnimator; 44 | import io.codetail.animation.ViewAnimationUtils; 45 | 46 | @SuppressWarnings("unused") 47 | public class PersistentSearchView extends RevealViewGroup { 48 | public static final int VOICE_RECOGNITION_CODE = 8185102; 49 | final static double COS_45 = Math.cos(Math.toRadians(45)); 50 | private static final int[] RES_IDS_ACTION_BAR_SIZE = { R.attr.actionBarSize }; 51 | private static final int DURATION_REVEAL_OPEN = 400; 52 | private static final int DURATION_REVEAL_CLOSE = 300; 53 | private static final int DURATION_HOME_BUTTON = 300; 54 | private static final int DURATION_LAYOUT_TRANSITION = 100; 55 | private HomeButton.IconState mHomeButtonCloseIconState; 56 | private HomeButton.IconState mHomeButtonOpenIconState; 57 | private HomeButton.IconState mHomeButtonSearchIconState; 58 | private SearchViewState mCurrentState; 59 | private SearchViewState mLastState; 60 | private DisplayMode mDisplayMode; 61 | private int mHomeButtonMode; 62 | private int mCardVerticalPadding; 63 | private int mCardHorizontalPadding; 64 | private int mCardHeight; 65 | private int mCustomToolbarHeight; 66 | private int mSearchCardElevation; 67 | private int mFromX, mFromY, mDesireRevealWidth; 68 | // Views 69 | private LogoView mLogoView; 70 | private CardView mSearchCardView; 71 | private HomeButton mHomeButton; 72 | private EditText mSearchEditText; 73 | private ListView mSuggestionListView; 74 | private ImageView mMicButton; 75 | private SearchListener mSearchListener; 76 | private HomeButtonListener mHomeButtonListener; 77 | private FrameLayout mRootLayout; 78 | private VoiceRecognitionDelegate mVoiceRecognitionDelegate; 79 | private boolean mAvoidTriggerTextWatcher; 80 | private boolean mIsMic; 81 | private int mSearchTextColor; 82 | private int mArrorButtonColor; 83 | private Drawable mLogoDrawable; 84 | private String mStringLogoDrawable; 85 | private int mSearchEditTextColor; 86 | private String mSearchEditTextHint; 87 | private int mSearchEditTextHintColor; 88 | private SearchSuggestionsBuilder mSuggestionBuilder; 89 | private SearchItemAdapter mSearchItemAdapter; 90 | private ArrayList mSearchSuggestions; 91 | private KeyboardView mCustomKeyboardView; 92 | private boolean showCustomKeyboard; 93 | 94 | public PersistentSearchView(Context context) { 95 | super(context); 96 | init(null); 97 | } 98 | public PersistentSearchView(Context context, AttributeSet attrs) { 99 | super(context, attrs); 100 | init(attrs); 101 | } 102 | 103 | public PersistentSearchView(Context context, AttributeSet attrs, int defStyle) { 104 | super(context, attrs, defStyle); 105 | init(attrs); 106 | } 107 | 108 | public void setCustomKeyboardView(KeyboardView customKeyboardView) 109 | { 110 | mCustomKeyboardView = customKeyboardView; 111 | } 112 | 113 | public void enableCustomKeyboardView(boolean enable) 114 | { 115 | showCustomKeyboard = enable; 116 | } 117 | 118 | static float calculateVerticalPadding(CardView cardView) { 119 | float maxShadowSize = cardView.getMaxCardElevation(); 120 | float cornerRadius = cardView.getRadius(); 121 | boolean addPaddingForCorners = cardView.getPreventCornerOverlap(); 122 | 123 | if (addPaddingForCorners) { 124 | return (float) (maxShadowSize * 1.5f + (1 - COS_45) * cornerRadius); 125 | } else { 126 | return maxShadowSize * 1.5f; 127 | } 128 | } 129 | 130 | static float calculateHorizontalPadding(CardView cardView) { 131 | float maxShadowSize = cardView.getMaxCardElevation(); 132 | float cornerRadius = cardView.getRadius(); 133 | boolean addPaddingForCorners = cardView.getPreventCornerOverlap(); 134 | if (addPaddingForCorners) { 135 | return (float) (maxShadowSize + (1 - COS_45) * cornerRadius); 136 | } else { 137 | return maxShadowSize; 138 | } 139 | } 140 | 141 | /** Calculates the Toolbar height in pixels. */ 142 | static int calculateToolbarSize(Context context) { 143 | if (context == null) { 144 | return 0; 145 | } 146 | 147 | Resources.Theme curTheme = context.getTheme(); 148 | if (curTheme == null) { 149 | return 0; 150 | } 151 | 152 | TypedArray att = curTheme.obtainStyledAttributes(RES_IDS_ACTION_BAR_SIZE); 153 | if (att == null) { 154 | return 0; 155 | } 156 | 157 | float size = att.getDimension(0, 0); 158 | att.recycle(); 159 | return (int)size; 160 | } 161 | 162 | private void init(AttributeSet attrs) { 163 | setSaveEnabled(true); 164 | LayoutInflater.from(getContext()).inflate(R.layout.layout_searchview, this, true); 165 | if (attrs != null) { 166 | TypedArray attrsValue = getContext().obtainStyledAttributes(attrs, 167 | R.styleable.PersistentSearchView); 168 | mDisplayMode = DisplayMode.fromInt(attrsValue.getInt(R.styleable.PersistentSearchView_persistentSV_displayMode, DisplayMode.MENUITEM.toInt())); 169 | mSearchCardElevation = attrsValue.getDimensionPixelSize(R.styleable.PersistentSearchView_persistentSV_searchCardElevation, -1); 170 | mSearchTextColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_searchTextColor, Color.BLACK); 171 | mLogoDrawable = attrsValue.getDrawable(R.styleable.PersistentSearchView_persistentSV_logoDrawable); 172 | mStringLogoDrawable = attrsValue.getString(R.styleable.PersistentSearchView_persistentSV_logoString); 173 | mSearchEditTextColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_editTextColor, Color.BLACK); 174 | mSearchEditTextHint = attrsValue.getString(R.styleable.PersistentSearchView_persistentSV_editHintText); 175 | mSearchEditTextHintColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_editHintTextColor, Color.BLACK); 176 | mArrorButtonColor = attrsValue.getColor(R.styleable.PersistentSearchView_persistentSV_homeButtonColor, Color.BLACK); 177 | mCustomToolbarHeight = attrsValue.getDimensionPixelSize(R.styleable.PersistentSearchView_persistentSV_customToolbarHeight, calculateToolbarSize(getContext())); 178 | mHomeButtonMode = attrsValue.getInt(R.styleable.PersistentSearchView_persistentSV_homeButtonMode, 0); 179 | attrsValue.recycle(); 180 | } 181 | 182 | if (mSearchCardElevation < 0) { 183 | mSearchCardElevation = getContext().getResources().getDimensionPixelSize(R.dimen.search_card_default_card_elevation); 184 | } 185 | 186 | mCardHeight = getResources().getDimensionPixelSize(R.dimen.search_card_height); 187 | mCardVerticalPadding = (mCustomToolbarHeight - mCardHeight) / 2; 188 | 189 | switch (mDisplayMode) { 190 | case MENUITEM: 191 | default: 192 | mCardHorizontalPadding = getResources().getDimensionPixelSize(R.dimen.search_card_visible_padding_menu_item_mode); 193 | if(mCardVerticalPadding > mCardHorizontalPadding) 194 | mCardHorizontalPadding = mCardVerticalPadding; 195 | mHomeButtonCloseIconState = HomeButton.IconState.ARROW; 196 | mHomeButtonOpenIconState = HomeButton.IconState.ARROW; 197 | setCurrentState(SearchViewState.NORMAL); 198 | break; 199 | case TOOLBAR: 200 | if(mHomeButtonMode == 0) { // Arrow Mode 201 | mHomeButtonCloseIconState = HomeButton.IconState.ARROW; 202 | mHomeButtonOpenIconState = HomeButton.IconState.ARROW; 203 | } else { // Burger Mode 204 | mHomeButtonCloseIconState = HomeButton.IconState.BURGER; 205 | mHomeButtonOpenIconState = HomeButton.IconState.ARROW; 206 | } 207 | mCardHorizontalPadding = getResources().getDimensionPixelSize(R.dimen.search_card_visible_padding_toolbar_mode); 208 | setCurrentState(SearchViewState.NORMAL); 209 | break; 210 | } 211 | mHomeButtonSearchIconState = HomeButton.IconState.ARROW; 212 | 213 | bindViews(); 214 | setValuesToViews(); 215 | 216 | this.mIsMic = true; 217 | mSearchSuggestions = new ArrayList<>(); 218 | mSearchItemAdapter = new SearchItemAdapter(getContext(), mSearchSuggestions); 219 | mSuggestionListView.setAdapter(mSearchItemAdapter); 220 | 221 | setUpLayoutTransition(); 222 | setUpListeners(); 223 | } 224 | 225 | public ArrayList getSearchSuggestions() { 226 | return mSearchSuggestions; 227 | } 228 | 229 | private void bindViews() { 230 | this.mSearchCardView = (CardView) findViewById(R.id.cardview_search); 231 | this.mHomeButton = (HomeButton) findViewById(R.id.button_home); 232 | this.mLogoView = (LogoView) findViewById(R.id.logoview); 233 | this.mSearchEditText = (EditText) findViewById(R.id.edittext_search); 234 | this.mSuggestionListView = (ListView) findViewById(R.id.listview_suggestions); 235 | this.mMicButton = (ImageView) findViewById(R.id.button_mic); 236 | } 237 | 238 | private void setValuesToViews() { 239 | this.mSearchCardView.setCardElevation(mSearchCardElevation); 240 | this.mSearchCardView.setMaxCardElevation(mSearchCardElevation); 241 | this.mHomeButton.setArrowDrawableColor(mArrorButtonColor); 242 | this.mHomeButton.setState(mHomeButtonCloseIconState); 243 | this.mHomeButton.setAnimationDuration(DURATION_HOME_BUTTON); 244 | this.mSearchEditText.setTextColor(mSearchEditTextColor); 245 | this.mSearchEditText.setHint(mSearchEditTextHint); 246 | this.mSearchEditText.setHintTextColor(mSearchEditTextHintColor); 247 | if (mLogoDrawable != null) { 248 | this.mLogoView.setLogo(mLogoDrawable); 249 | } 250 | else if (mStringLogoDrawable != null) 251 | { 252 | this.mLogoView.setLogo(mStringLogoDrawable); 253 | } 254 | this.mLogoView.setTextColor(mSearchTextColor); 255 | } 256 | 257 | private void setUpListeners() { 258 | mHomeButton.setOnClickListener(new OnClickListener() { 259 | @Override 260 | public void onClick(View v) { 261 | if (mCurrentState == SearchViewState.EDITING) { 262 | cancelEditing(); 263 | } else if (mCurrentState == SearchViewState.SEARCH) { 264 | fromSearchToNormal(); 265 | } else { 266 | if (mHomeButtonListener != null) 267 | mHomeButtonListener.onHomeButtonClick(); 268 | } 269 | } 270 | 271 | }); 272 | mLogoView.setOnClickListener(new OnClickListener() { 273 | 274 | @Override 275 | public void onClick(View v) { 276 | dispatchStateChange(SearchViewState.EDITING); // This would call when state is wrong. 277 | } 278 | 279 | }); 280 | mSearchEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { 281 | public boolean onEditorAction(TextView v, int actionId, 282 | KeyEvent event) { 283 | if (actionId == EditorInfo.IME_ACTION_SEARCH) { 284 | clearSuggestions(); 285 | fromEditingToSearch(true, false); 286 | return true; 287 | } 288 | return false; 289 | } 290 | }); 291 | mSearchEditText.setOnKeyListener(new OnKeyListener() { 292 | public boolean onKey(View v, int keyCode, KeyEvent event) { 293 | if (keyCode == KeyEvent.KEYCODE_ENTER) { 294 | clearSuggestions(); 295 | fromEditingToSearch(true, false); 296 | return true; 297 | } else if (keyCode == KeyEvent.KEYCODE_BACK) { 298 | return mSearchListener != null && mSearchListener.onSearchEditBackPressed(); 299 | } 300 | return false; 301 | } 302 | }); 303 | micStateChanged(); 304 | mMicButton.setOnClickListener(new OnClickListener() { 305 | @Override 306 | public void onClick(View v) { 307 | micClick(); 308 | } 309 | }); 310 | mSearchEditText.addTextChangedListener(new TextWatcher() { 311 | 312 | @Override 313 | public void afterTextChanged(Editable s) { 314 | if (!mAvoidTriggerTextWatcher) { 315 | if (s.length() > 0) { 316 | showClearButton(); 317 | buildSearchSuggestions(getSearchText()); 318 | } else { 319 | showMicButton(); 320 | buildEmptySearchSuggestions(); 321 | } 322 | } 323 | if (mSearchListener != null) 324 | mSearchListener.onSearchTermChanged(s.toString()); 325 | } 326 | 327 | @Override 328 | public void beforeTextChanged(CharSequence s, int start, int count, 329 | int after) { 330 | } 331 | 332 | @Override 333 | public void onTextChanged(CharSequence s, int start, int before, 334 | int count) { 335 | 336 | } 337 | 338 | }); 339 | 340 | } 341 | 342 | private void setUpLayoutTransition() { 343 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { 344 | RelativeLayout searchRoot = (RelativeLayout) findViewById(R.id.search_root); 345 | LayoutTransition layoutTransition = new LayoutTransition(); 346 | layoutTransition.setDuration(DURATION_LAYOUT_TRANSITION); 347 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) { 348 | // layoutTransition.enableTransitionType(LayoutTransition.CHANGING); 349 | layoutTransition.enableTransitionType(LayoutTransition.CHANGE_DISAPPEARING); 350 | layoutTransition.setStartDelay(LayoutTransition.CHANGING, 0); 351 | } 352 | layoutTransition.setStartDelay(LayoutTransition.CHANGE_DISAPPEARING, 0); 353 | mSearchCardView.setLayoutTransition(layoutTransition); 354 | } 355 | } 356 | 357 | @Override 358 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 359 | final int widthSize = MeasureSpec.getSize(widthMeasureSpec); 360 | final int heightSize = MeasureSpec.getSize(heightMeasureSpec); 361 | 362 | int totalHeight = 0; 363 | int searchCardWidth; 364 | final int childCount = getChildCount(); 365 | for (int i = 0; i < childCount; ++i) { 366 | final View child = getChildAt(i); 367 | if (child.getVisibility() != GONE) { 368 | if (i == 0 && child instanceof CardView) { 369 | CardView searchCard = (CardView) child; 370 | int horizontalPadding = (int) Math.ceil(calculateHorizontalPadding(searchCard)); 371 | int verticalPadding = (int) Math.ceil(calculateVerticalPadding(searchCard)); 372 | // searchCardWidth = widthSize - 2 * mCardVisiblePadding + horizontalPadding * 2; 373 | int searchCardLeft = mCardHorizontalPadding - horizontalPadding; 374 | // searchCardTop = mCardVisiblePadding - verticalPadding; 375 | searchCardWidth = widthSize - searchCardLeft * 2; 376 | int cardWidthSpec = MeasureSpec.makeMeasureSpec(searchCardWidth, MeasureSpec.EXACTLY); 377 | // int cardHeightSpec = MeasureSpec.makeMeasureSpec(searchCardHeight, MeasureSpec.EXACTLY); 378 | measureChild(child, cardWidthSpec, heightMeasureSpec); 379 | int childMeasuredHeight = child.getMeasuredHeight(); 380 | int childMeasuredWidth = child.getMeasuredWidth(); 381 | int childHeight = childMeasuredHeight - verticalPadding * 2; 382 | totalHeight = totalHeight + childHeight + mCardVerticalPadding * 2; 383 | } 384 | } 385 | } 386 | if(totalHeight < mCustomToolbarHeight) 387 | totalHeight = mCustomToolbarHeight; 388 | setMeasuredDimension(widthSize, totalHeight); 389 | } 390 | 391 | @Override 392 | protected void onLayout(boolean changed, int l, int t, int r, int b) { 393 | final int count = getChildCount(); 394 | int searchViewWidth = r - l; 395 | int searchViewHeight = b - t; 396 | int searchCardLeft; 397 | int searchCardTop; 398 | int searchCardRight; 399 | int searchCardBottom; 400 | int searchCardWidth; 401 | int searchCardHeight; 402 | for (int i = 0; i < count; i++) { 403 | View child = getChildAt(i); 404 | if (i == 0 && child instanceof CardView) { 405 | CardView searchCard = (CardView) child; 406 | int horizontalPadding = (int) Math.ceil(calculateHorizontalPadding(searchCard)); 407 | int verticalPadding = (int) Math.ceil(calculateVerticalPadding(searchCard)); 408 | searchCardLeft = mCardHorizontalPadding - horizontalPadding; 409 | searchCardTop = mCardVerticalPadding - verticalPadding; 410 | searchCardWidth = searchViewWidth - searchCardLeft * 2; 411 | searchCardHeight = child.getMeasuredHeight(); 412 | searchCardRight = searchCardLeft + searchCardWidth; 413 | searchCardBottom = searchCardTop + searchCardHeight; 414 | child.layout(searchCardLeft, searchCardTop, searchCardRight, searchCardBottom); 415 | } 416 | } 417 | } 418 | 419 | private void revealFromMenuItem() { 420 | setVisibility(View.VISIBLE); 421 | revealFrom(mFromX, mFromY, mDesireRevealWidth); 422 | } 423 | 424 | private void hideCircularlyToMenuItem() { 425 | if (mFromX == 0 || mFromY == 0) { 426 | mFromX = getRight(); 427 | mFromY = getTop(); 428 | } 429 | hideCircularly(mFromX, mFromY); 430 | } 431 | 432 | /*** 433 | * Hide the PersistentSearchView using the circle animation. Can be called regardless of result list length 434 | */ 435 | private void hideCircularly(int x, int y) { 436 | 437 | Resources r = getResources(); 438 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96, 439 | r.getDisplayMetrics()); 440 | int finalRadius = (int) Math.max(this.getMeasuredWidth() * 1.5, px); 441 | 442 | SupportAnimator animator = ViewAnimationUtils.createCircularReveal( 443 | mSearchCardView, x, y, 0, finalRadius); 444 | animator = animator.reverse(); 445 | animator.setInterpolator(new AccelerateDecelerateInterpolator()); 446 | animator.setDuration(DURATION_REVEAL_CLOSE); 447 | animator.start(); 448 | animator.addListener(new SupportAnimator.AnimatorListener() { 449 | 450 | @Override 451 | public void onAnimationStart() { 452 | 453 | } 454 | 455 | @Override 456 | public void onAnimationEnd() { 457 | setVisibility(View.GONE); 458 | closeSearchInternal(); 459 | // closeSearch(); 460 | } 461 | 462 | @Override 463 | public void onAnimationCancel() { 464 | 465 | } 466 | 467 | @Override 468 | public void onAnimationRepeat() { 469 | 470 | } 471 | 472 | }); 473 | } 474 | 475 | private void hideCircularly() { 476 | hideCircularly(getLeft() + getRight(), getTop()); 477 | } 478 | 479 | public boolean getSearchOpen() { 480 | return getVisibility() == VISIBLE && (mCurrentState == SearchViewState.SEARCH || mCurrentState == SearchViewState.EDITING); 481 | } 482 | 483 | /*** 484 | * Hide the search suggestions manually 485 | */ 486 | public void hideSuggestions() { 487 | this.mSearchEditText.setVisibility(View.GONE); 488 | this.mSuggestionListView.setVisibility(View.GONE); 489 | } 490 | 491 | private boolean isMicEnabled() { 492 | return mVoiceRecognitionDelegate != null; 493 | } 494 | 495 | private void micStateChanged() { 496 | mMicButton.setVisibility((!mIsMic || isMicEnabled()) ? VISIBLE : INVISIBLE); 497 | } 498 | 499 | private void micStateChanged(boolean isMic) { 500 | this.mIsMic = isMic; 501 | micStateChanged(); 502 | } 503 | 504 | private void showMicButton() { 505 | micStateChanged(true); 506 | mMicButton.setImageDrawable( 507 | ResourcesCompat.getDrawable(getResources(), R.drawable.ic_action_mic_black, null)); 508 | } 509 | 510 | private void showClearButton() { 511 | micStateChanged(false); 512 | mMicButton.setImageDrawable( 513 | ResourcesCompat.getDrawable(getResources(), R.drawable.ic_action_clear_black, null)); 514 | } 515 | 516 | /*** 517 | * Mandatory method for the onClick event 518 | */ 519 | public void micClick() { 520 | if (!mIsMic) { 521 | setSearchString("", false); 522 | } else { 523 | if (mVoiceRecognitionDelegate != null) 524 | mVoiceRecognitionDelegate.onStartVoiceRecognition(); 525 | } 526 | } 527 | 528 | /*** 529 | * Populate the PersistentSearchView with words, in an ArrayList. Used by the voice input 530 | * 531 | * @param matches Matches 532 | */ 533 | public void populateEditText(ArrayList matches) { 534 | String text = matches.get(0).trim(); 535 | populateEditText(text); 536 | } 537 | 538 | /*** 539 | * Populate the PersistentSearchView with search query 540 | * 541 | * @param query Matches 542 | */ 543 | public void populateEditText(String query) { 544 | String text = query.trim(); 545 | setSearchString(text, true); 546 | dispatchStateChange(SearchViewState.SEARCH); 547 | } 548 | 549 | /*** 550 | * Set whether the menu button should be shown. Particularly useful for apps that adapt to screen sizes 551 | * 552 | * @param visibility Whether to show 553 | */ 554 | 555 | public void setHomeButtonVisibility(int visibility) { 556 | this.mHomeButton.setVisibility(visibility); 557 | } 558 | 559 | /*** 560 | * Set the menu listener 561 | * 562 | * @param homeButtonListener MenuListener 563 | */ 564 | public void setHomeButtonListener(HomeButtonListener homeButtonListener) { 565 | this.mHomeButtonListener = homeButtonListener; 566 | } 567 | 568 | /*** 569 | * Set the search listener 570 | * 571 | * @param listener SearchListener 572 | */ 573 | public void setSearchListener(SearchListener listener) { 574 | this.mSearchListener = listener; 575 | } 576 | 577 | /*** 578 | * Set the text color of the logo 579 | * 580 | * @param color logo text color 581 | */ 582 | public void setLogoTextColor(int color) { 583 | mLogoView.setTextColor(color); 584 | } 585 | 586 | /*** 587 | * Get the PersistentSearchView's current text 588 | * 589 | * @return Text 590 | */ 591 | public String getSearchText() { 592 | return mSearchEditText.getText().toString(); 593 | } 594 | 595 | public void clearSuggestions() { 596 | mSearchItemAdapter.clear(); 597 | } 598 | 599 | /*** 600 | * Set the PersistentSearchView's current text manually 601 | * 602 | * @param text Text 603 | * @param avoidTriggerTextWatcher avoid trigger TextWatcher(TextChangedListener) 604 | */ 605 | public void setSearchString(String text, boolean avoidTriggerTextWatcher) { 606 | if (avoidTriggerTextWatcher) 607 | mAvoidTriggerTextWatcher = true; 608 | mSearchEditText.setText(""); 609 | mSearchEditText.append(text); 610 | mAvoidTriggerTextWatcher = false; 611 | } 612 | 613 | private void buildEmptySearchSuggestions() { 614 | if (mSuggestionBuilder != null) { 615 | mSearchSuggestions.clear(); 616 | Collection suggestions = mSuggestionBuilder.buildEmptySearchSuggestion(10); 617 | if (suggestions != null && suggestions.size() > 0) { 618 | mSearchSuggestions.addAll(suggestions); 619 | } 620 | mSearchItemAdapter.notifyDataSetChanged(); 621 | } 622 | } 623 | 624 | private void buildSearchSuggestions(String query) { 625 | if (mSuggestionBuilder != null) { 626 | mSearchSuggestions.clear(); 627 | Collection suggestions = mSuggestionBuilder.buildSearchSuggestion(10, query); 628 | if (suggestions != null && suggestions.size() > 0) { 629 | mSearchSuggestions.addAll(suggestions); 630 | } 631 | mSearchItemAdapter.notifyDataSetChanged(); 632 | } 633 | } 634 | 635 | private void revealFrom(float x, float y, int desireRevealWidth) { 636 | Resources r = getResources(); 637 | float px = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 96, 638 | r.getDisplayMetrics()); 639 | if(desireRevealWidth <= 0) 640 | desireRevealWidth = getMeasuredWidth(); 641 | if(desireRevealWidth <= 0) { 642 | DisplayMetrics metrics = getResources().getDisplayMetrics(); 643 | desireRevealWidth = metrics.widthPixels; 644 | } 645 | if(x <= 0 ) 646 | x = desireRevealWidth - mCardHeight / 2; 647 | if(y <= 0) 648 | y = mCardHeight / 2; 649 | 650 | int measuredHeight = getMeasuredWidth(); 651 | int finalRadius = (int) Math.max(Math.max(measuredHeight, px), desireRevealWidth); 652 | 653 | SupportAnimator animator = ViewAnimationUtils.createCircularReveal( 654 | mSearchCardView, (int) x, (int) y, 0, finalRadius); 655 | animator.setInterpolator(new AccelerateDecelerateInterpolator()); 656 | animator.setDuration(DURATION_REVEAL_OPEN); 657 | animator.addListener(new SupportAnimator.AnimatorListener() { 658 | 659 | @Override 660 | public void onAnimationCancel() { 661 | 662 | } 663 | 664 | @Override 665 | public void onAnimationEnd() { 666 | // show search view here 667 | openSearchInternal(true); 668 | } 669 | 670 | @Override 671 | public void onAnimationRepeat() { 672 | 673 | } 674 | 675 | @Override 676 | public void onAnimationStart() { 677 | 678 | } 679 | 680 | }); 681 | animator.start(); 682 | } 683 | 684 | private void search() { 685 | String searchTerm = getSearchText(); 686 | if (!TextUtils.isEmpty(searchTerm)) { 687 | setLogoTextInt(searchTerm); 688 | if (mSearchListener != null) 689 | mSearchListener.onSearch(searchTerm); 690 | } 691 | } 692 | 693 | private void openSearchInternal(Boolean openKeyboard) { 694 | this.mLogoView.setVisibility(View.GONE); 695 | this.mSearchEditText.setVisibility(View.VISIBLE); 696 | mSearchEditText.requestFocus(); 697 | this.mSuggestionListView.setVisibility(View.VISIBLE); 698 | mSuggestionListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { 699 | 700 | @Override 701 | public void onItemClick(AdapterView arg0, View arg1, int arg2, long arg3) { 702 | hideKeyboard(); 703 | SearchItem result = mSearchSuggestions.get(arg2); 704 | if (mSearchListener != null) { 705 | if (mSearchListener.onSuggestion(result)) { 706 | setSearchString(result.getValue(), true); 707 | fromEditingToSearch(true, false); 708 | } 709 | } else { 710 | setSearchString(result.getValue(), true); 711 | fromEditingToSearch(true, false); 712 | } 713 | } 714 | 715 | }); 716 | String currentSearchText = getSearchText(); 717 | if (currentSearchText.length() > 0) { 718 | buildSearchSuggestions(currentSearchText); 719 | } else { 720 | buildEmptySearchSuggestions(); 721 | } 722 | 723 | if (mSearchListener != null) 724 | mSearchListener.onSearchEditOpened(); 725 | if (getSearchText().length() > 0) { 726 | showClearButton(); 727 | } 728 | if (openKeyboard) { 729 | if(showCustomKeyboard && mCustomKeyboardView != null) { // Show custom keyboard 730 | mCustomKeyboardView.setVisibility(View.VISIBLE); 731 | mCustomKeyboardView.setEnabled(true); 732 | 733 | // Enable cursor, but still prevent default keyboard from showing up 734 | OnTouchListener otl = new OnTouchListener() { 735 | @Override 736 | public boolean onTouch(View v, MotionEvent event) { 737 | switch (event.getAction()) { 738 | case MotionEvent.ACTION_DOWN: 739 | mCustomKeyboardView.setVisibility(View.VISIBLE); 740 | mCustomKeyboardView.setEnabled(true); 741 | Layout layout = ((EditText) v).getLayout(); 742 | float x = event.getX() + mSearchEditText.getScrollX(); 743 | int offset = layout.getOffsetForHorizontal(0, x); 744 | if (offset > 0) 745 | if (x > layout.getLineMax(0)) 746 | mSearchEditText.setSelection(offset); // Touch was at the end of the text 747 | else 748 | mSearchEditText.setSelection(offset - 1); 749 | break; 750 | case MotionEvent.ACTION_MOVE: 751 | layout = ((EditText) v).getLayout(); 752 | x = event.getX() + mSearchEditText.getScrollX(); 753 | offset = layout.getOffsetForHorizontal(0, x); 754 | if (offset > 0) 755 | if (x > layout.getLineMax(0)) 756 | mSearchEditText.setSelection(offset); // Touch point was at the end of the text 757 | else 758 | mSearchEditText.setSelection(offset - 1); 759 | break; 760 | } 761 | return true; 762 | } 763 | }; 764 | mSearchEditText.setOnTouchListener(otl); 765 | } else { // Show default keyboard 766 | mSearchEditText.setOnTouchListener(null); 767 | InputMethodManager inputMethodManager = (InputMethodManager) getContext() 768 | .getSystemService(Context.INPUT_METHOD_SERVICE); 769 | inputMethodManager.toggleSoftInputFromWindow( 770 | getApplicationWindowToken(), 771 | InputMethodManager.SHOW_FORCED, 0); 772 | } 773 | } 774 | } 775 | 776 | private void closeSearchInternal() { 777 | this.mLogoView.setVisibility(View.VISIBLE); 778 | this.mSearchEditText.setVisibility(View.GONE); 779 | // if(mDisplayMode == DISPLAY_MODE_AS_TOOLBAR) { 780 | mSuggestionListView.setVisibility(View.GONE); 781 | // } 782 | // this.mSuggestionListView.setVisibility(View.GONE); 783 | /*if (mTintView != null && mRootLayout != null) { 784 | mRootLayout.removeView(mTintView); 785 | }*/ 786 | if (mSearchListener != null) 787 | mSearchListener.onSearchEditClosed(); 788 | showMicButton(); 789 | 790 | hideKeyboard(); 791 | } 792 | 793 | private void hideKeyboard() { 794 | if(showCustomKeyboard && mCustomKeyboardView != null) { 795 | mCustomKeyboardView.setVisibility(View.GONE); 796 | mCustomKeyboardView.setEnabled(false); 797 | } 798 | else { 799 | ((InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(getApplicationWindowToken(), 0); 800 | } 801 | } 802 | 803 | public boolean isEditing() { 804 | return mCurrentState == SearchViewState.EDITING; 805 | } 806 | 807 | public boolean isSearching() { 808 | return mCurrentState == SearchViewState.EDITING || mCurrentState == SearchViewState.SEARCH; 809 | } 810 | 811 | private void setLogoTextInt(String text) { 812 | mLogoView.setText(text); 813 | } 814 | 815 | public void setHomeButtonOpenIconState(HomeButton.IconState homeButtonOpenIconState) { 816 | this.mHomeButtonOpenIconState = homeButtonOpenIconState; 817 | } 818 | 819 | public void setHomeButtonCloseIconState(HomeButton.IconState homeButtonCloseIconState) { 820 | this.mHomeButtonCloseIconState = homeButtonCloseIconState; 821 | } 822 | 823 | public void setSuggestionBuilder(SearchSuggestionsBuilder suggestionBuilder) { 824 | this.mSuggestionBuilder = suggestionBuilder; 825 | } 826 | 827 | private void fromNormalToEditing() { 828 | if(mDisplayMode == DisplayMode.TOOLBAR) { 829 | setCurrentState(SearchViewState.EDITING); 830 | openSearchInternal(true); 831 | } else if(mDisplayMode == DisplayMode.MENUITEM) { 832 | setCurrentState(SearchViewState.EDITING); 833 | if(ViewCompat.isAttachedToWindow(this)) 834 | revealFromMenuItem(); 835 | else { 836 | getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 837 | @Override 838 | public void onGlobalLayout() { 839 | if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { 840 | getViewTreeObserver().removeGlobalOnLayoutListener(this); 841 | } else { 842 | getViewTreeObserver().removeOnGlobalLayoutListener(this); 843 | } 844 | revealFromMenuItem(); 845 | } 846 | }); 847 | } 848 | 849 | } 850 | mHomeButton.animateState(mHomeButtonOpenIconState); 851 | } 852 | 853 | private void fromNormalToSearch() { 854 | if(mDisplayMode == DisplayMode.TOOLBAR) { 855 | setCurrentState(SearchViewState.SEARCH); 856 | search(); 857 | } else if(mDisplayMode == DisplayMode.MENUITEM) { 858 | setVisibility(VISIBLE); 859 | fromEditingToSearch(); 860 | } 861 | mHomeButton.animateState(mHomeButtonSearchIconState); 862 | } 863 | 864 | private void fromSearchToNormal() { 865 | setLogoTextInt(""); 866 | setSearchString("", true); 867 | setCurrentState(SearchViewState.NORMAL); 868 | if(mDisplayMode == DisplayMode.TOOLBAR) { 869 | closeSearchInternal(); 870 | } else if(mDisplayMode == DisplayMode.MENUITEM) { 871 | hideCircularlyToMenuItem(); 872 | } 873 | setLogoTextInt(""); 874 | if (mSearchListener != null) 875 | mSearchListener.onSearchExit(); 876 | mHomeButton.animateState(mHomeButtonCloseIconState); 877 | } 878 | 879 | private void fromSearchToEditing() { 880 | openSearchInternal(true); 881 | setCurrentState(SearchViewState.EDITING); 882 | mHomeButton.animateState(mHomeButtonOpenIconState); 883 | } 884 | 885 | private void fromEditingToNormal() { 886 | setCurrentState(SearchViewState.NORMAL); 887 | if(mDisplayMode == DisplayMode.TOOLBAR) { 888 | setSearchString("", false); 889 | closeSearchInternal(); 890 | } else if(mDisplayMode == DisplayMode.MENUITEM) { 891 | setSearchString("", false); 892 | hideCircularlyToMenuItem(); 893 | } 894 | setLogoTextInt(""); 895 | if (mSearchListener != null) 896 | mSearchListener.onSearchExit(); 897 | mHomeButton.animateState(mHomeButtonCloseIconState); 898 | } 899 | 900 | private void fromEditingToSearch() { 901 | fromEditingToSearch(false, false); 902 | } 903 | 904 | private void fromEditingToSearch(boolean avoidSearch) { 905 | fromEditingToSearch(false, avoidSearch); 906 | } 907 | 908 | private void fromEditingToSearch(boolean forceSearch, boolean avoidSearch) { 909 | if(TextUtils.isEmpty(getSearchText())) { 910 | fromEditingToNormal(); 911 | } else { 912 | setCurrentState(SearchViewState.SEARCH); 913 | if((!getSearchText().equals(mLogoView.getText()) || forceSearch) && !avoidSearch) { 914 | search(); 915 | } 916 | closeSearchInternal(); 917 | mHomeButton.animateState(mHomeButtonSearchIconState); 918 | } 919 | } 920 | 921 | private void dispatchStateChange(SearchViewState targetState) { 922 | if(targetState == SearchViewState.NORMAL) { 923 | if (mCurrentState == SearchViewState.EDITING) { 924 | fromEditingToNormal(); 925 | } else if(mCurrentState == SearchViewState.SEARCH) { 926 | fromSearchToNormal(); 927 | } 928 | } else if(targetState == SearchViewState.EDITING) { 929 | if (mCurrentState == SearchViewState.NORMAL) { 930 | fromNormalToEditing(); 931 | } else if(mCurrentState == SearchViewState.SEARCH) { 932 | fromSearchToEditing(); 933 | } 934 | } else if(targetState == SearchViewState.SEARCH) { 935 | if (mCurrentState == SearchViewState.NORMAL) { 936 | fromNormalToSearch(); 937 | } else if(mCurrentState == SearchViewState.EDITING) { 938 | fromEditingToSearch(); 939 | } 940 | } 941 | } 942 | 943 | private void setCurrentState(SearchViewState state) { 944 | mLastState = mCurrentState; 945 | mCurrentState = state; 946 | } 947 | 948 | public void openSearch() { 949 | dispatchStateChange(SearchViewState.EDITING); 950 | } 951 | 952 | public void setStartPositionFromMenuItem(View menuItemView) { 953 | DisplayMetrics metrics = getResources().getDisplayMetrics(); 954 | int width = metrics.widthPixels; 955 | setStartPositionFromMenuItem(menuItemView, width); 956 | } 957 | 958 | public void setStartPositionFromMenuItem(View menuItemView, int desireRevealWidth) { 959 | if (menuItemView != null) { 960 | int[] location = new int[2]; 961 | menuItemView.getLocationInWindow(location); 962 | int menuItemWidth = menuItemView.getWidth(); 963 | this.mFromX = location[0] + menuItemWidth / 2; 964 | this.mFromY = location[1]; 965 | this.mDesireRevealWidth = desireRevealWidth; 966 | } 967 | } 968 | 969 | public void openSearch(String query) { 970 | setSearchString(query, true); 971 | dispatchStateChange(SearchViewState.SEARCH); 972 | } 973 | 974 | public void closeSearch() { 975 | dispatchStateChange(SearchViewState.NORMAL); 976 | } 977 | 978 | public void cancelEditing() { 979 | if(TextUtils.isEmpty(mLogoView.getText())) { 980 | fromEditingToNormal(); 981 | } else { 982 | fromEditingToSearch(true); 983 | } 984 | } 985 | 986 | public void setVoiceRecognitionDelegate(VoiceRecognitionDelegate delegate) { 987 | this.mVoiceRecognitionDelegate = delegate; 988 | micStateChanged(); 989 | } 990 | 991 | public enum DisplayMode { 992 | MENUITEM(0), TOOLBAR(1); 993 | int mode; 994 | 995 | DisplayMode(int mode) { 996 | this.mode = mode; 997 | } 998 | 999 | public static DisplayMode fromInt(int mode) { 1000 | for (DisplayMode enumMode : values()) { 1001 | if (enumMode.mode == mode) return enumMode; 1002 | } 1003 | throw new IllegalArgumentException(); 1004 | } 1005 | 1006 | public int toInt() { 1007 | return mode; 1008 | } 1009 | } 1010 | 1011 | public enum SearchViewState { 1012 | NORMAL(0), EDITING(1), SEARCH(2); 1013 | int state; 1014 | SearchViewState(int state) { 1015 | this.state = state; 1016 | } 1017 | public static SearchViewState fromInt(int state) { 1018 | for (SearchViewState enumState : values()) { 1019 | if (enumState.state == state) return enumState; 1020 | } 1021 | throw new IllegalArgumentException(); 1022 | } 1023 | 1024 | public int toInt() { 1025 | return state; 1026 | } 1027 | } 1028 | 1029 | public interface SearchListener { 1030 | 1031 | /** 1032 | * Called when a suggestion is pressed is pressed 1033 | */ 1034 | boolean onSuggestion(SearchItem searchItem); 1035 | 1036 | /** 1037 | * Called when the clear button is pressed 1038 | */ 1039 | void onSearchCleared(); 1040 | 1041 | /** 1042 | * Called when the PersistentSearchView's EditText text changes 1043 | */ 1044 | void onSearchTermChanged(String term); 1045 | 1046 | /** 1047 | * Called when search happens 1048 | * 1049 | * @param query search string 1050 | */ 1051 | void onSearch(String query); 1052 | 1053 | /** 1054 | * Called when search state change to SEARCH and EditText, Suggestions visible 1055 | */ 1056 | void onSearchEditOpened(); 1057 | 1058 | /** 1059 | * Called when search state change from SEARCH and EditText, Suggestions gone 1060 | */ 1061 | void onSearchEditClosed(); 1062 | 1063 | /** 1064 | * Called when edit text get focus and backpressed 1065 | */ 1066 | boolean onSearchEditBackPressed(); 1067 | 1068 | /** 1069 | * Called when search back to start state. 1070 | */ 1071 | void onSearchExit(); 1072 | } 1073 | 1074 | public interface HomeButtonListener { 1075 | /** 1076 | * Called when the menu button is pressed 1077 | */ 1078 | void onHomeButtonClick(); 1079 | } 1080 | 1081 | @Override 1082 | public Parcelable onSaveInstanceState() { 1083 | Parcelable superState = super.onSaveInstanceState(); 1084 | SavedState ss = new SavedState(superState, mCurrentState); 1085 | ss.childrenStates = new SparseArray(); 1086 | for (int i = 0; i < getChildCount(); i++) { 1087 | getChildAt(i).saveHierarchyState(ss.childrenStates); 1088 | } 1089 | return ss; 1090 | } 1091 | 1092 | @Override 1093 | public void onRestoreInstanceState(Parcelable state) { 1094 | if(!(state instanceof SavedState)) { 1095 | super.onRestoreInstanceState(state); 1096 | return; 1097 | } 1098 | this.mAvoidTriggerTextWatcher = true; 1099 | SavedState ss = (SavedState) state; 1100 | super.onRestoreInstanceState(ss.getSuperState()); 1101 | for (int i = 0; i < getChildCount(); i++) { 1102 | getChildAt(i).restoreHierarchyState(ss.childrenStates); 1103 | } 1104 | dispatchStateChange(ss.getCurrentSearchViewState()); 1105 | this.mAvoidTriggerTextWatcher = false; 1106 | } 1107 | 1108 | @Override 1109 | protected void dispatchSaveInstanceState(SparseArray container) { 1110 | dispatchFreezeSelfOnly(container); 1111 | } 1112 | 1113 | @Override 1114 | protected void dispatchRestoreInstanceState(SparseArray container) { 1115 | dispatchThawSelfOnly(container); 1116 | } 1117 | 1118 | static class SavedState extends BaseSavedState { 1119 | SparseArray childrenStates; 1120 | private SearchViewState mCurrentSearchViewState; 1121 | 1122 | SavedState(Parcelable superState, SearchViewState currentSearchViewState) { 1123 | super(superState); 1124 | mCurrentSearchViewState = currentSearchViewState; 1125 | } 1126 | 1127 | private SavedState(Parcel in, ClassLoader classLoader) { 1128 | super(in); 1129 | childrenStates = in.readSparseArray(classLoader); 1130 | mCurrentSearchViewState = SearchViewState.fromInt(in.readInt()); 1131 | } 1132 | 1133 | @Override 1134 | public void writeToParcel(Parcel out, int flags) { 1135 | super.writeToParcel(out, flags); 1136 | out.writeSparseArray(childrenStates); 1137 | out.writeInt(mCurrentSearchViewState.toInt()); 1138 | } 1139 | 1140 | public SearchViewState getCurrentSearchViewState() { 1141 | return mCurrentSearchViewState; 1142 | } 1143 | 1144 | public static final ClassLoaderCreator CREATOR 1145 | = new ClassLoaderCreator() { 1146 | @Override 1147 | public SavedState createFromParcel(Parcel source, ClassLoader loader) { 1148 | return new SavedState(source, loader); 1149 | } 1150 | 1151 | @Override 1152 | public SavedState createFromParcel(Parcel source) { 1153 | return createFromParcel(null); 1154 | } 1155 | 1156 | public SavedState[] newArray(int size) { 1157 | return new SavedState[size]; 1158 | } 1159 | }; 1160 | } 1161 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/RevealViewGroup.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.graphics.Rect; 7 | import android.util.AttributeSet; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | 11 | import io.codetail.animation.RevealAnimator; 12 | import io.codetail.animation.SupportAnimator; 13 | import io.codetail.animation.ViewAnimationUtils; 14 | 15 | public abstract class RevealViewGroup extends ViewGroup implements RevealAnimator { 16 | 17 | private Path mRevealPath; 18 | private final Rect mTargetBounds = new Rect(); 19 | private RevealInfo mRevealInfo; 20 | private boolean mRunning; 21 | private float mRadius; 22 | 23 | public RevealViewGroup(Context context) { 24 | this(context, null); 25 | } 26 | 27 | public RevealViewGroup(Context context, AttributeSet attrs) { 28 | this(context, attrs, 0); 29 | } 30 | 31 | public RevealViewGroup(Context context, AttributeSet attrs, int defStyle) { 32 | super(context, attrs); 33 | mRevealPath = new Path(); 34 | } 35 | 36 | @Override 37 | public void onRevealAnimationStart() { 38 | mRunning = true; 39 | } 40 | 41 | @Override 42 | public void onRevealAnimationEnd() { 43 | mRunning = false; 44 | invalidate(mTargetBounds); 45 | } 46 | 47 | @Override 48 | public void onRevealAnimationCancel() { 49 | onRevealAnimationEnd(); 50 | } 51 | 52 | /** 53 | * Circle radius size 54 | * 55 | * @hide 56 | */ 57 | @Override 58 | public void setRevealRadius(float radius){ 59 | mRadius = radius; 60 | mRevealInfo.getTarget().getHitRect(mTargetBounds); 61 | invalidate(mTargetBounds); 62 | } 63 | 64 | /** 65 | * Circle radius size 66 | * 67 | * @hide 68 | */ 69 | @Override 70 | public float getRevealRadius(){ 71 | return mRadius; 72 | } 73 | 74 | /** 75 | * @hide 76 | */ 77 | @Override 78 | public void attachRevealInfo(RevealInfo info) { 79 | mRevealInfo = info; 80 | } 81 | 82 | /** 83 | * @hide 84 | */ 85 | @Override 86 | public SupportAnimator startReverseAnimation() { 87 | if(mRevealInfo != null && mRevealInfo.hasTarget() && !mRunning) { 88 | return ViewAnimationUtils.createCircularReveal(mRevealInfo.getTarget(), 89 | mRevealInfo.centerX, mRevealInfo.centerY, 90 | mRevealInfo.endRadius, mRevealInfo.startRadius); 91 | } 92 | return null; 93 | } 94 | 95 | @Override 96 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 97 | if(mRunning && child == mRevealInfo.getTarget()){ 98 | final int state = canvas.save(); 99 | 100 | mRevealPath.reset(); 101 | mRevealPath.addCircle(mRevealInfo.centerX, mRevealInfo.centerY, mRadius, Path.Direction.CW); 102 | 103 | canvas.clipPath(mRevealPath); 104 | 105 | boolean isInvalided = super.drawChild(canvas, child, drawingTime); 106 | 107 | canvas.restoreToCount(state); 108 | 109 | return isInvalided; 110 | } 111 | 112 | return super.drawChild(canvas, child, drawingTime); 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/SearchItem.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.graphics.drawable.Drawable; 4 | 5 | public class SearchItem { 6 | private String mTitle; 7 | private String mValue; 8 | private Drawable mIcon; 9 | private int mType; 10 | public static final int TYPE_SEARCH_ITEM_HISTORY = 0; 11 | public static final int TYPE_SEARCH_ITEM_SUGGESTION = 1; 12 | public static final int TYPE_SEARCH_ITEM_OPTION = 2; 13 | public static final int TYPE_SEARCH_ITEM_CUSTOM = 3; 14 | public static final int TYPE_SEARCH_ITEM_DEFAULT = TYPE_SEARCH_ITEM_HISTORY; 15 | 16 | 17 | public SearchItem(String title, String value) { 18 | this(title, value, TYPE_SEARCH_ITEM_DEFAULT, null); 19 | } 20 | /** 21 | * Create a search result with text and an icon 22 | * @param title display value 23 | * @param value inner value for search 24 | * @param type item type 25 | */ 26 | public SearchItem(String title, String value, int type) { 27 | this(title, value, type, null); 28 | } 29 | 30 | public SearchItem(String title, String value, int type, Drawable icon) { 31 | this.mTitle = title; 32 | this.mValue = value; 33 | this.mType = type; 34 | this.mIcon = icon; 35 | } 36 | 37 | /** 38 | * Return the title of the result 39 | */ 40 | @Override 41 | public String toString() { 42 | return mTitle; 43 | } 44 | 45 | public String getTitle() { 46 | return mTitle; 47 | } 48 | 49 | public void setTitle(String title) { 50 | this.mTitle = title; 51 | } 52 | 53 | public String getValue() { 54 | return mValue; 55 | } 56 | 57 | public void setValue(String value) { 58 | this.mValue = value; 59 | } 60 | 61 | public Drawable getIcon() { 62 | return mIcon; 63 | } 64 | 65 | public void setIcon(Drawable icon) { 66 | this.mIcon = icon; 67 | } 68 | 69 | public int getType() { 70 | return mType; 71 | } 72 | 73 | public void setType(int type) { 74 | this.mType = type; 75 | } 76 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/SearchItemAdapter.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.content.Context; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ArrayAdapter; 8 | import android.widget.ImageView; 9 | import android.widget.TextView; 10 | 11 | import java.util.ArrayList; 12 | 13 | public class SearchItemAdapter extends ArrayAdapter { 14 | 15 | public SearchItemAdapter(Context context, ArrayList options) { 16 | super(context, 0, options); 17 | } 18 | 19 | @Override 20 | public View getView(int position, View convertView, ViewGroup parent) { 21 | SearchItem searchItem = getItem(position); 22 | if (convertView == null) { 23 | convertView = LayoutInflater.from(getContext()).inflate( 24 | R.layout.layout_searchitem, parent, false); 25 | } 26 | 27 | View border = convertView.findViewById(R.id.view_border); 28 | if (position == 0) { 29 | border.setVisibility(View.VISIBLE); 30 | } else { 31 | border.setVisibility(View.GONE); 32 | } 33 | final TextView title = (TextView) convertView 34 | .findViewById(R.id.textview_title); 35 | title.setText(searchItem.getTitle()); 36 | ImageView icon = (ImageView) convertView.findViewById(R.id.imageview_icon); 37 | if(searchItem.getIcon() == null) { 38 | switch (searchItem.getType()) { 39 | case SearchItem.TYPE_SEARCH_ITEM_HISTORY: 40 | icon.setImageResource(R.drawable.ic_history_black); 41 | break; 42 | default: 43 | case SearchItem.TYPE_SEARCH_ITEM_SUGGESTION: 44 | icon.setImageResource(R.drawable.ic_search_black); 45 | break; 46 | } 47 | } else { 48 | icon.setImageDrawable(searchItem.getIcon()); 49 | } 50 | return convertView; 51 | } 52 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/SearchSuggestionsBuilder.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import java.util.Collection; 4 | 5 | public interface SearchSuggestionsBuilder { 6 | Collection buildEmptySearchSuggestion(int maxCount); 7 | Collection buildSearchSuggestion(int maxCount, String query); 8 | } 9 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/SimpleSearchListener.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | public class SimpleSearchListener implements PersistentSearchView.SearchListener { 4 | 5 | @Override 6 | public boolean onSuggestion(SearchItem searchItem) { 7 | return true; 8 | } 9 | 10 | @Override 11 | public void onSearchCleared() { 12 | 13 | } 14 | 15 | @Override 16 | public void onSearchTermChanged(String term) { 17 | 18 | } 19 | 20 | @Override 21 | public void onSearch(String query) { 22 | 23 | } 24 | 25 | @Override 26 | public void onSearchEditOpened() { 27 | 28 | } 29 | 30 | @Override 31 | public void onSearchEditClosed() { 32 | 33 | } 34 | 35 | @Override 36 | public boolean onSearchEditBackPressed() { 37 | return false; 38 | } 39 | 40 | @Override 41 | public void onSearchExit() { 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/TextDrawable.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.content.res.Resources; 4 | import android.graphics.Canvas; 5 | import android.graphics.Color; 6 | import android.graphics.ColorFilter; 7 | import android.graphics.Paint; 8 | import android.graphics.PixelFormat; 9 | import android.graphics.Rect; 10 | import android.graphics.drawable.Drawable; 11 | import android.util.TypedValue; 12 | 13 | public class TextDrawable extends Drawable { 14 | 15 | private final String mText; 16 | private final Paint mPaint; 17 | private int mIntrinsicWidth; 18 | private int mIntrinsicHeight; 19 | private float mTextSize; 20 | 21 | public TextDrawable(Resources resources, String text) { 22 | 23 | this.mText = text; 24 | 25 | mTextSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 26 | 24f, resources.getDisplayMetrics()); 27 | 28 | this.mPaint = new Paint(); 29 | mPaint.setColor(Color.GRAY); 30 | mPaint.setTextSize(mTextSize); 31 | mPaint.setAntiAlias(true); 32 | mPaint.setFakeBoldText(false); 33 | mPaint.setShadowLayer(2f, 0, 0, Color.BLACK); 34 | mPaint.setStyle(Paint.Style.FILL); 35 | mPaint.setTextAlign(Paint.Align.LEFT); 36 | 37 | Rect bounds = new Rect(); 38 | mPaint.getTextBounds(mText, 0, mText.length(), bounds); 39 | mIntrinsicWidth = bounds.width(); 40 | mIntrinsicHeight = bounds.height(); 41 | } 42 | 43 | @Override 44 | public void draw(Canvas canvas) { 45 | Rect r = getBounds(); 46 | 47 | int count = canvas.save(); 48 | canvas.translate(r.left, r.top); 49 | int height = canvas.getHeight() < 0 ? r.height() : canvas.getHeight(); 50 | canvas.drawText(mText, 0, height / 2 - ((mPaint.descent() + mPaint.ascent()) / 2), mPaint); 51 | canvas.restoreToCount(count); 52 | } 53 | 54 | @Override 55 | public int getIntrinsicWidth() { 56 | return mIntrinsicWidth; 57 | } 58 | @Override 59 | public int getIntrinsicHeight() { 60 | return mIntrinsicHeight; 61 | } 62 | 63 | @Override 64 | public void setAlpha(int alpha) { 65 | mPaint.setAlpha(alpha); 66 | } 67 | 68 | @Override 69 | public void setColorFilter(ColorFilter cf) { 70 | mPaint.setColorFilter(cf); 71 | } 72 | 73 | @Override 74 | public int getOpacity() { 75 | return PixelFormat.TRANSLUCENT; 76 | } 77 | } -------------------------------------------------------------------------------- /persistentsearchview/src/main/java/org/cryse/widget/persistentsearch/VoiceRecognitionDelegate.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | 7 | public abstract class VoiceRecognitionDelegate { 8 | public static final int DEFAULT_VOICE_REQUEST_CODE = 8185102; 9 | private int mVoiceRecognitionRequestCode; 10 | private Activity mActivity; 11 | private android.app.Fragment mFragment; 12 | private android.support.v4.app.Fragment mSupportFragment; 13 | 14 | public VoiceRecognitionDelegate(Activity activity) { 15 | this(activity, DEFAULT_VOICE_REQUEST_CODE); 16 | } 17 | 18 | public VoiceRecognitionDelegate(Activity activity, int activityRequestCode) { 19 | this.mActivity = activity; 20 | this.mVoiceRecognitionRequestCode = activityRequestCode; 21 | } 22 | 23 | public VoiceRecognitionDelegate(android.app.Fragment fragment) { 24 | this(fragment, DEFAULT_VOICE_REQUEST_CODE); 25 | } 26 | 27 | public VoiceRecognitionDelegate(android.app.Fragment fragment, int activityRequestCode) { 28 | this.mFragment = fragment; 29 | this.mVoiceRecognitionRequestCode = activityRequestCode; 30 | } 31 | 32 | public VoiceRecognitionDelegate(android.support.v4.app.Fragment supportFragment) { 33 | this(supportFragment, DEFAULT_VOICE_REQUEST_CODE); 34 | } 35 | 36 | public VoiceRecognitionDelegate(android.support.v4.app.Fragment supportFragment, int activityRequestCode) { 37 | this.mSupportFragment = supportFragment; 38 | this.mVoiceRecognitionRequestCode = activityRequestCode; 39 | } 40 | 41 | public void onStartVoiceRecognition() { 42 | if (mActivity != null) { 43 | Intent intent = buildVoiceRecognitionIntent(); 44 | mActivity.startActivityForResult(intent, mVoiceRecognitionRequestCode); 45 | } else if(mFragment != null) { 46 | Intent intent = buildVoiceRecognitionIntent(); 47 | mFragment.startActivityForResult(intent, mVoiceRecognitionRequestCode); 48 | } else if(mSupportFragment != null) { 49 | Intent intent = buildVoiceRecognitionIntent(); 50 | mSupportFragment.startActivityForResult(intent, mVoiceRecognitionRequestCode); 51 | } 52 | } 53 | 54 | protected Context getContext() { 55 | if (mActivity != null) { 56 | return mActivity; 57 | } else if(mFragment != null) { 58 | return mFragment.getContext(); 59 | } else if(mSupportFragment != null) { 60 | return mSupportFragment.getContext(); 61 | } else { 62 | throw new IllegalStateException("Could not get context in VoiceRecognitionDelegate."); 63 | } 64 | } 65 | 66 | public abstract Intent buildVoiceRecognitionIntent(); 67 | 68 | public abstract boolean isVoiceRecognitionAvailable(); 69 | } 70 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-hdpi/ic_action_clear_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-hdpi/ic_action_clear_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-hdpi/ic_action_mic_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-hdpi/ic_action_mic_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-hdpi/ic_history_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-hdpi/ic_history_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-hdpi/ic_search_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-hdpi/ic_search_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-mdpi/ic_action_clear_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-mdpi/ic_action_clear_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-mdpi/ic_action_mic_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-mdpi/ic_action_mic_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-mdpi/ic_history_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-mdpi/ic_history_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-mdpi/ic_search_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-mdpi/ic_search_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xhdpi/ic_action_clear_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xhdpi/ic_action_clear_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xhdpi/ic_action_mic_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xhdpi/ic_action_mic_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xhdpi/ic_history_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xhdpi/ic_history_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xhdpi/ic_search_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xhdpi/ic_search_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxhdpi/ic_action_clear_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxhdpi/ic_action_clear_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxhdpi/ic_action_mic_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxhdpi/ic_action_mic_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxhdpi/ic_history_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxhdpi/ic_history_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxhdpi/ic_search_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxhdpi/ic_search_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxxhdpi/ic_action_clear_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxxhdpi/ic_action_clear_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxxhdpi/ic_action_mic_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxxhdpi/ic_action_mic_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxxhdpi/ic_history_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxxhdpi/ic_history_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/drawable-xxxhdpi/ic_search_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/persistentsearchview/src/main/res/drawable-xxxhdpi/ic_search_black.png -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/layout/layout_searchitem.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 34 | 35 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/layout/layout_searchview.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 20 | 21 | 24 | 25 | 32 | 33 | 49 | 50 | 69 | 70 | 71 | 72 | 73 | 81 | 82 | 83 | 84 | 90 | 91 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8dp 4 | 4dp 5 | 2dp 6 | 48dp 7 | -------------------------------------------------------------------------------- /persistentsearchview/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Speak now 4 | 5 | 6 | -------------------------------------------------------------------------------- /sample-apk/sample.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample-apk/sample.apk -------------------------------------------------------------------------------- /sample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion appCompileSdkVersion 5 | buildToolsVersion appBuildToolsVersion 6 | defaultConfig { 7 | applicationId "org.cryse.widget.persistentsearch.sample" 8 | minSdkVersion appMinSdkVersion 9 | targetSdkVersion appTargetSdkVersion 10 | versionCode appVersionCode 11 | versionName appVersionName 12 | signingConfig signingConfigs.debug 13 | } 14 | signingConfigs { 15 | debug {} 16 | release {} 17 | } 18 | buildTypes { 19 | debug { 20 | debuggable true 21 | signingConfig signingConfigs.debug 22 | } 23 | release { 24 | minifyEnabled true 25 | shrinkResources true 26 | signingConfig signingConfigs.release 27 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 28 | } 29 | } 30 | } 31 | 32 | dependencies { 33 | compile fileTree(dir: 'libs', include: ['*.jar']) 34 | compile project(':persistentsearchview') 35 | compile "com.android.support:appcompat-v7:$supportlibraryVersion" 36 | compile "com.android.support:recyclerview-v7:$supportlibraryVersion" 37 | compile "com.android.support:cardview-v7:$supportlibraryVersion" 38 | } 39 | 40 | 41 | def Properties props = new Properties() 42 | def propFile = new File('sample/signing.properties') 43 | if (propFile.canRead()){ 44 | props.load(new FileInputStream(propFile)) 45 | 46 | if (props!=null && props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') && 47 | props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) { 48 | android.signingConfigs.debug.storeFile = file(props['STORE_FILE']) 49 | android.signingConfigs.debug.storePassword = props['STORE_PASSWORD'] 50 | android.signingConfigs.debug.keyAlias = props['KEY_ALIAS'] 51 | android.signingConfigs.debug.keyPassword = props['KEY_PASSWORD'] 52 | android.signingConfigs.release.storeFile = file(props['STORE_FILE']) 53 | android.signingConfigs.release.storePassword = props['STORE_PASSWORD'] 54 | android.signingConfigs.release.keyAlias = props['KEY_ALIAS'] 55 | android.signingConfigs.release.keyPassword = props['KEY_PASSWORD'] 56 | } else { 57 | println 'signing.properties found but some entries are missing' 58 | android.buildTypes.debug.signingConfig = null 59 | android.buildTypes.release.signingConfig = null 60 | } 61 | } else { 62 | println 'signing.properties not found' 63 | android.buildTypes.debug.signingConfig = null 64 | android.buildTypes.release.signingConfig = null 65 | } -------------------------------------------------------------------------------- /sample/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/Kieron/Android/adt-bundle-windows-x86_64-20140702/adt-bundle-windows-x86_64-20140702/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /sample/src/androidTest/java/org/cryse/widget/persistentsearch/sample/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /sample/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.animation.Animator; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Bundle; 8 | import android.speech.RecognizerIntent; 9 | import android.support.v7.widget.DefaultItemAnimator; 10 | import android.support.v7.widget.LinearLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.util.Log; 13 | import android.view.View; 14 | import android.widget.Button; 15 | import android.widget.Toast; 16 | 17 | import org.cryse.widget.persistentsearch.DefaultVoiceRecognizerDelegate; 18 | import org.cryse.widget.persistentsearch.PersistentSearchView; 19 | import org.cryse.widget.persistentsearch.PersistentSearchView.HomeButtonListener; 20 | import org.cryse.widget.persistentsearch.PersistentSearchView.SearchListener; 21 | import org.cryse.widget.persistentsearch.SearchItem; 22 | import org.cryse.widget.persistentsearch.VoiceRecognitionDelegate; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | public class MainActivity extends Activity { 28 | private static final int VOICE_RECOGNITION_REQUEST_CODE = 1023; 29 | private PersistentSearchView mSearchView; 30 | private Button mMenuItemSampleButton; 31 | private Button mSearchSampleButton; 32 | private Button mGithubRepoButton; 33 | private View mSearchTintView; 34 | private SearchResultAdapter mResultAdapter; 35 | private RecyclerView mRecyclerView; 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_main); 40 | mSearchView = (PersistentSearchView) findViewById(R.id.searchview); 41 | mSearchTintView = findViewById(R.id.view_search_tint); 42 | mMenuItemSampleButton = (Button) findViewById(R.id.button_reveal_sample); 43 | mSearchSampleButton = (Button) findViewById(R.id.button_search_sample); 44 | mGithubRepoButton = (Button) findViewById(R.id.button_github_repo); 45 | mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_search_result); 46 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 47 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 48 | mResultAdapter = new SearchResultAdapter(new ArrayList()); 49 | mRecyclerView.setAdapter(mResultAdapter); 50 | mMenuItemSampleButton.setOnClickListener(new View.OnClickListener() { 51 | @Override 52 | public void onClick(View v) { 53 | startActivity(new Intent(MainActivity.this, MenuItemSampleActivity.class)); 54 | } 55 | }); 56 | mSearchSampleButton.setOnClickListener(new View.OnClickListener() { 57 | @Override 58 | public void onClick(View v) { 59 | startActivity(new Intent(MainActivity.this, SearchActivity.class)); 60 | } 61 | }); 62 | mGithubRepoButton.setOnClickListener(new View.OnClickListener() { 63 | @Override 64 | public void onClick(View v) { 65 | Intent i = new Intent(Intent.ACTION_VIEW); 66 | i.setData(Uri.parse(getString(R.string.url_github_repo))); 67 | startActivity(i); 68 | } 69 | }); 70 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE); 71 | if(delegate.isVoiceRecognitionAvailable()) { 72 | mSearchView.setVoiceRecognitionDelegate(delegate); 73 | } 74 | mSearchView.setHomeButtonListener(new HomeButtonListener() { 75 | 76 | @Override 77 | public void onHomeButtonClick() { 78 | //Hamburger has been clicked 79 | Toast.makeText(MainActivity.this, "Menu click", Toast.LENGTH_LONG).show(); 80 | } 81 | 82 | }); 83 | mSearchTintView.setOnClickListener(new View.OnClickListener() { 84 | @Override 85 | public void onClick(View v) { 86 | mSearchView.cancelEditing(); 87 | } 88 | }); 89 | mSearchView.setSuggestionBuilder(new SampleSuggestionsBuilder(this)); 90 | mSearchView.setSearchListener(new SearchListener() { 91 | 92 | @Override 93 | public void onSearchEditOpened() { 94 | //Use this to tint the screen 95 | mSearchTintView.setVisibility(View.VISIBLE); 96 | mSearchTintView 97 | .animate() 98 | .alpha(1.0f) 99 | .setDuration(300) 100 | .setListener(new SimpleAnimationListener()) 101 | .start(); 102 | } 103 | 104 | @Override 105 | public void onSearchEditClosed() { 106 | mSearchTintView 107 | .animate() 108 | .alpha(0.0f) 109 | .setDuration(300) 110 | .setListener(new SimpleAnimationListener() { 111 | @Override 112 | public void onAnimationEnd(Animator animation) { 113 | super.onAnimationEnd(animation); 114 | mSearchTintView.setVisibility(View.GONE); 115 | } 116 | }) 117 | .start(); 118 | } 119 | 120 | @Override 121 | public boolean onSearchEditBackPressed() { 122 | return false; 123 | } 124 | 125 | @Override 126 | public void onSearchExit() { 127 | mResultAdapter.clear(); 128 | if(mRecyclerView.getVisibility() == View.VISIBLE) { 129 | mRecyclerView.setVisibility(View.GONE); 130 | } 131 | } 132 | 133 | @Override 134 | public void onSearchTermChanged(String term) { 135 | 136 | } 137 | 138 | @Override 139 | public void onSearch(String string) { 140 | Toast.makeText(MainActivity.this, string +" Searched", Toast.LENGTH_LONG).show(); 141 | mRecyclerView.setVisibility(View.VISIBLE); 142 | fillResultToRecyclerView(string); 143 | } 144 | 145 | @Override 146 | public boolean onSuggestion(SearchItem searchItem) { 147 | Log.d("onSuggestion", searchItem.getTitle()); 148 | return false; 149 | } 150 | 151 | @Override 152 | public void onSearchCleared() { 153 | //Called when the clear button is clicked 154 | } 155 | 156 | }); 157 | } 158 | 159 | @Override 160 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 161 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) { 162 | ArrayList matches = data 163 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); 164 | mSearchView.populateEditText(matches); 165 | } 166 | super.onActivityResult(requestCode, resultCode, data); 167 | } 168 | 169 | private void fillResultToRecyclerView(String query) { 170 | List newResults = new ArrayList<>(); 171 | for(int i =0; i< 10; i++) { 172 | SearchResult result = new SearchResult(query, query + Integer.toString(i), ""); 173 | newResults.add(result); 174 | } 175 | mResultAdapter.replaceWith(newResults); 176 | } 177 | 178 | @Override 179 | public void onBackPressed() { 180 | if(mSearchView.isSearching()) { 181 | mSearchView.closeSearch(); 182 | } else if(mRecyclerView.getVisibility() == View.VISIBLE) { 183 | mResultAdapter.clear(); 184 | mRecyclerView.setVisibility(View.GONE); 185 | } else { 186 | super.onBackPressed(); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/MenuItemSampleActivity.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.animation.Animator; 4 | import android.content.Intent; 5 | import android.os.Bundle; 6 | import android.speech.RecognizerIntent; 7 | import android.support.v7.app.ActionBar; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.DefaultItemAnimator; 10 | import android.support.v7.widget.LinearLayoutManager; 11 | import android.support.v7.widget.RecyclerView; 12 | import android.support.v7.widget.Toolbar; 13 | import android.util.Log; 14 | import android.view.Menu; 15 | import android.view.MenuInflater; 16 | import android.view.MenuItem; 17 | import android.view.View; 18 | import android.widget.Toast; 19 | 20 | import org.cryse.widget.persistentsearch.DefaultVoiceRecognizerDelegate; 21 | import org.cryse.widget.persistentsearch.PersistentSearchView; 22 | import org.cryse.widget.persistentsearch.PersistentSearchView.HomeButtonListener; 23 | import org.cryse.widget.persistentsearch.PersistentSearchView.SearchListener; 24 | import org.cryse.widget.persistentsearch.SearchItem; 25 | import org.cryse.widget.persistentsearch.VoiceRecognitionDelegate; 26 | 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | public class MenuItemSampleActivity extends AppCompatActivity { 31 | private static final int VOICE_RECOGNITION_REQUEST_CODE = 1023; 32 | private PersistentSearchView mSearchView; 33 | private Toolbar mToolbar; 34 | private MenuItem mSearchMenuItem; 35 | private View mSearchTintView; 36 | private SearchResultAdapter mResultAdapter; 37 | private RecyclerView mRecyclerView; 38 | 39 | @Override 40 | public void onCreate(Bundle savedInstanceState) { 41 | super.onCreate(savedInstanceState); 42 | setContentView(R.layout.activity_menu_item_sample); 43 | mSearchView = (PersistentSearchView) findViewById(R.id.searchview); 44 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE); 45 | if(delegate.isVoiceRecognitionAvailable()) { 46 | mSearchView.setVoiceRecognitionDelegate(delegate); 47 | } 48 | mToolbar = (Toolbar) findViewById(R.id.toolbar); 49 | mSearchTintView = findViewById(R.id.view_search_tint); 50 | this.setSupportActionBar(mToolbar); 51 | ActionBar actionBar = getSupportActionBar(); 52 | if (actionBar != null) { 53 | actionBar.setDisplayShowHomeEnabled(true); 54 | actionBar.setDisplayHomeAsUpEnabled(true); 55 | } 56 | setUpSearchView(); 57 | } 58 | 59 | @Override 60 | public boolean onCreateOptionsMenu(Menu menu) { 61 | MenuInflater inflater = getMenuInflater(); 62 | inflater.inflate(R.menu.menu_searchview, menu); 63 | mSearchMenuItem = menu.findItem(R.id.action_search); 64 | return true; 65 | } 66 | 67 | @Override 68 | public boolean onOptionsItemSelected(MenuItem item) { 69 | switch (item.getItemId()) { 70 | case android.R.id.home: 71 | finish(); 72 | return true; 73 | case R.id.action_search: 74 | if(mSearchMenuItem != null) { 75 | openSearch(); 76 | return true; 77 | } else { 78 | return false; 79 | } 80 | } 81 | return super.onOptionsItemSelected(item); 82 | } 83 | 84 | public void openSearch() { 85 | View menuItemView = findViewById(R.id.action_search); 86 | mSearchView.setStartPositionFromMenuItem(menuItemView); 87 | mSearchView.openSearch(); 88 | } 89 | 90 | public void setUpSearchView() { 91 | mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_search_result); 92 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 93 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 94 | mResultAdapter = new SearchResultAdapter(new ArrayList()); 95 | mRecyclerView.setAdapter(mResultAdapter); 96 | mSearchTintView.setOnClickListener(new View.OnClickListener() { 97 | @Override 98 | public void onClick(View v) { 99 | mSearchView.cancelEditing(); 100 | } 101 | }); 102 | mSearchView.setHomeButtonListener(new HomeButtonListener() { 103 | 104 | @Override 105 | public void onHomeButtonClick() { 106 | // Hamburger has been clicked 107 | Toast.makeText(MenuItemSampleActivity.this, "Menu click", 108 | Toast.LENGTH_LONG).show(); 109 | } 110 | 111 | }); 112 | mSearchView.setSuggestionBuilder(new SampleSuggestionsBuilder(this)); 113 | mSearchView.setSearchListener(new SearchListener() { 114 | 115 | @Override 116 | public boolean onSuggestion(SearchItem searchItem) { 117 | Log.d("onSuggestion", searchItem.getTitle()); 118 | return false; 119 | } 120 | 121 | @Override 122 | public void onSearchEditOpened() { 123 | //Use this to tint the screen 124 | mSearchTintView.setVisibility(View.VISIBLE); 125 | mSearchTintView 126 | .animate() 127 | .alpha(1.0f) 128 | .setDuration(300) 129 | .setListener(new SimpleAnimationListener()) 130 | .start(); 131 | 132 | } 133 | 134 | @Override 135 | public void onSearchEditClosed() { 136 | mSearchTintView 137 | .animate() 138 | .alpha(0.0f) 139 | .setDuration(300) 140 | .setListener(new SimpleAnimationListener() { 141 | @Override 142 | public void onAnimationEnd(Animator animation) { 143 | super.onAnimationEnd(animation); 144 | mSearchTintView.setVisibility(View.GONE); 145 | } 146 | }) 147 | .start(); 148 | } 149 | 150 | @Override 151 | public boolean onSearchEditBackPressed() { 152 | if(mSearchView.isEditing()) { 153 | mSearchView.cancelEditing(); 154 | return true; 155 | } 156 | return false; 157 | } 158 | 159 | @Override 160 | public void onSearchExit() { 161 | mResultAdapter.clear(); 162 | if (mRecyclerView.getVisibility() == View.VISIBLE) { 163 | mRecyclerView.setVisibility(View.GONE); 164 | } 165 | } 166 | 167 | @Override 168 | public void onSearchTermChanged(String term) { 169 | 170 | } 171 | 172 | @Override 173 | public void onSearch(String string) { 174 | Toast.makeText(MenuItemSampleActivity.this, string + " Searched", Toast.LENGTH_LONG).show(); 175 | mRecyclerView.setVisibility(View.VISIBLE); 176 | fillResultToRecyclerView(string); 177 | } 178 | 179 | @Override 180 | public void onSearchCleared() { 181 | 182 | } 183 | 184 | }); 185 | 186 | } 187 | 188 | private void fillResultToRecyclerView(String query) { 189 | List newResults = new ArrayList<>(); 190 | for(int i =0; i< 10; i++) { 191 | SearchResult result = new SearchResult(query, query + Integer.toString(i), ""); 192 | newResults.add(result); 193 | } 194 | mResultAdapter.replaceWith(newResults); 195 | } 196 | 197 | @Override 198 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 199 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) { 200 | ArrayList matches = data 201 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); 202 | mSearchView.populateEditText(matches); 203 | } 204 | super.onActivityResult(requestCode, resultCode, data); 205 | } 206 | 207 | @Override 208 | public void onBackPressed() { 209 | if(mSearchView.isSearching()) { 210 | mSearchView.closeSearch(); 211 | } else if(mRecyclerView.getVisibility() == View.VISIBLE) { 212 | mResultAdapter.clear(); 213 | mRecyclerView.setVisibility(View.GONE); 214 | } else { 215 | super.onBackPressed(); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/SampleSuggestionsBuilder.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.content.Context; 4 | 5 | import org.cryse.widget.persistentsearch.SearchItem; 6 | import org.cryse.widget.persistentsearch.SearchSuggestionsBuilder; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import java.util.List; 11 | 12 | public class SampleSuggestionsBuilder implements SearchSuggestionsBuilder { 13 | private Context mContext; 14 | private List mHistorySuggestions = new ArrayList();; 15 | 16 | public SampleSuggestionsBuilder(Context context) { 17 | this.mContext = context; 18 | createHistorys(); 19 | } 20 | 21 | private void createHistorys() { 22 | SearchItem item1 = new SearchItem( 23 | "Isaac Newton", 24 | "Isaac Newton", 25 | SearchItem.TYPE_SEARCH_ITEM_HISTORY 26 | ); 27 | mHistorySuggestions.add(item1); 28 | SearchItem item2 = new SearchItem( 29 | "Albert Einstein", 30 | "Albert Einstein", 31 | SearchItem.TYPE_SEARCH_ITEM_HISTORY 32 | ); 33 | mHistorySuggestions.add(item2); 34 | SearchItem item3 = new SearchItem( 35 | "John von Neumann", 36 | "John von Neumann", 37 | SearchItem.TYPE_SEARCH_ITEM_HISTORY 38 | ); 39 | mHistorySuggestions.add(item3); 40 | SearchItem item4 = new SearchItem( 41 | "Alan Mathison Turing", 42 | "Alan Mathison Turing", 43 | SearchItem.TYPE_SEARCH_ITEM_HISTORY 44 | ); 45 | mHistorySuggestions.add(item4); 46 | } 47 | 48 | @Override 49 | public Collection buildEmptySearchSuggestion(int maxCount) { 50 | List items = new ArrayList(); 51 | items.addAll(mHistorySuggestions); 52 | return items; 53 | } 54 | 55 | @Override 56 | public Collection buildSearchSuggestion(int maxCount, String query) { 57 | List items = new ArrayList(); 58 | if(query.startsWith("@")) { 59 | SearchItem peopleSuggestion = new SearchItem( 60 | "Search People: " + query.substring(1), 61 | query, 62 | SearchItem.TYPE_SEARCH_ITEM_SUGGESTION 63 | ); 64 | items.add(peopleSuggestion); 65 | } else if(query.startsWith("#")) { 66 | SearchItem toppicSuggestion = new SearchItem( 67 | "Search Topic: " + query.substring(1), 68 | query, 69 | SearchItem.TYPE_SEARCH_ITEM_SUGGESTION 70 | ); 71 | items.add(toppicSuggestion); 72 | } else { 73 | SearchItem peopleSuggestion = new SearchItem( 74 | "Search People: " + query, 75 | "@" + query, 76 | SearchItem.TYPE_SEARCH_ITEM_SUGGESTION 77 | ); 78 | items.add(peopleSuggestion); 79 | SearchItem toppicSuggestion = new SearchItem( 80 | "Search Topic: " + query, 81 | "#" + query, 82 | SearchItem.TYPE_SEARCH_ITEM_SUGGESTION 83 | ); 84 | items.add(toppicSuggestion); 85 | } 86 | for(SearchItem item : mHistorySuggestions) { 87 | if(item.getValue().startsWith(query)) { 88 | items.add(item); 89 | } 90 | } 91 | return items; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/SearchActivity.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.animation.Animator; 4 | import android.app.Activity; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.speech.RecognizerIntent; 8 | import android.support.v7.widget.DefaultItemAnimator; 9 | import android.support.v7.widget.LinearLayoutManager; 10 | import android.support.v7.widget.RecyclerView; 11 | import android.util.Log; 12 | import android.view.View; 13 | import android.widget.Toast; 14 | 15 | import org.cryse.widget.persistentsearch.DefaultVoiceRecognizerDelegate; 16 | import org.cryse.widget.persistentsearch.PersistentSearchView; 17 | import org.cryse.widget.persistentsearch.PersistentSearchView.HomeButtonListener; 18 | import org.cryse.widget.persistentsearch.PersistentSearchView.SearchListener; 19 | import org.cryse.widget.persistentsearch.SearchItem; 20 | import org.cryse.widget.persistentsearch.VoiceRecognitionDelegate; 21 | 22 | import java.util.ArrayList; 23 | import java.util.List; 24 | 25 | public class SearchActivity extends Activity { 26 | private static final int VOICE_RECOGNITION_REQUEST_CODE = 1023; 27 | private PersistentSearchView mSearchView; 28 | private View mSearchTintView; 29 | private SearchResultAdapter mResultAdapter; 30 | private RecyclerView mRecyclerView; 31 | @Override 32 | protected void onCreate(Bundle savedInstanceState) { 33 | super.onCreate(savedInstanceState); 34 | setContentView(R.layout.activity_search); 35 | mSearchView = (PersistentSearchView) findViewById(R.id.searchview); 36 | mSearchTintView = findViewById(R.id.view_search_tint); 37 | mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview_search_result); 38 | mRecyclerView.setItemAnimator(new DefaultItemAnimator()); 39 | mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); 40 | mResultAdapter = new SearchResultAdapter(new ArrayList()); 41 | mRecyclerView.setAdapter(mResultAdapter); 42 | VoiceRecognitionDelegate delegate = new DefaultVoiceRecognizerDelegate(this, VOICE_RECOGNITION_REQUEST_CODE); 43 | if(delegate.isVoiceRecognitionAvailable()) { 44 | mSearchView.setVoiceRecognitionDelegate(delegate); 45 | } 46 | // mSearchView.openSearch("Text Query"); 47 | mSearchView.setHomeButtonListener(new HomeButtonListener() { 48 | 49 | @Override 50 | public void onHomeButtonClick() { 51 | //Hamburger has been clicked 52 | finish(); 53 | } 54 | 55 | }); 56 | mSearchTintView.setOnClickListener(new View.OnClickListener() { 57 | @Override 58 | public void onClick(View v) { 59 | mSearchView.cancelEditing(); 60 | } 61 | }); 62 | mSearchView.setSuggestionBuilder(new SampleSuggestionsBuilder(this)); 63 | mSearchView.setSearchListener(new SearchListener() { 64 | 65 | @Override 66 | public boolean onSuggestion(SearchItem searchItem) { 67 | Log.d("onSuggestion", searchItem.getTitle()); 68 | return false; 69 | } 70 | 71 | @Override 72 | public void onSearchEditOpened() { 73 | //Use this to tint the screen 74 | mSearchTintView.setVisibility(View.VISIBLE); 75 | mSearchTintView 76 | .animate() 77 | .alpha(1.0f) 78 | .setDuration(300) 79 | .setListener(new SimpleAnimationListener()) 80 | .start(); 81 | } 82 | 83 | @Override 84 | public void onSearchEditClosed() { 85 | //Use this to un-tint the screen 86 | mSearchTintView 87 | .animate() 88 | .alpha(0.0f) 89 | .setDuration(300) 90 | .setListener(new SimpleAnimationListener() { 91 | @Override 92 | public void onAnimationEnd(Animator animation) { 93 | super.onAnimationEnd(animation); 94 | mSearchTintView.setVisibility(View.GONE); 95 | } 96 | }) 97 | .start(); 98 | } 99 | 100 | @Override 101 | public boolean onSearchEditBackPressed() { 102 | return false; 103 | } 104 | 105 | @Override 106 | public void onSearchExit() { 107 | mResultAdapter.clear(); 108 | if (mRecyclerView.getVisibility() == View.VISIBLE) { 109 | mRecyclerView.setVisibility(View.GONE); 110 | } 111 | } 112 | 113 | @Override 114 | public void onSearchTermChanged(String term) { 115 | 116 | } 117 | 118 | @Override 119 | public void onSearch(String string) { 120 | Toast.makeText(SearchActivity.this, string + " Searched", Toast.LENGTH_LONG).show(); 121 | mRecyclerView.setVisibility(View.VISIBLE); 122 | fillResultToRecyclerView(string); 123 | } 124 | 125 | @Override 126 | public void onSearchCleared() { 127 | //Called when the clear button is clicked 128 | } 129 | 130 | }); 131 | mSearchView.openSearch("John von Neumann"); 132 | } 133 | 134 | @Override 135 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 136 | if (requestCode == VOICE_RECOGNITION_REQUEST_CODE && resultCode == RESULT_OK) { 137 | ArrayList matches = data 138 | .getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); 139 | mSearchView.populateEditText(matches); 140 | } 141 | super.onActivityResult(requestCode, resultCode, data); 142 | } 143 | 144 | private void fillResultToRecyclerView(String query) { 145 | List newResults = new ArrayList<>(); 146 | for(int i =0; i< 10; i++) { 147 | SearchResult result = new SearchResult(query, query + Integer.toString(i), ""); 148 | newResults.add(result); 149 | } 150 | mResultAdapter.replaceWith(newResults); 151 | } 152 | 153 | @Override 154 | public void onBackPressed() { 155 | if(mSearchView.isEditing()) { 156 | mSearchView.cancelEditing(); 157 | } else { 158 | super.onBackPressed(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/SearchFragment.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | /** 4 | * Created by tyk55 on 8/17/2015. 5 | */ 6 | public class SearchFragment { 7 | } 8 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/SearchResult.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | public class SearchResult { 4 | private String title; 5 | private String description; 6 | private String iconUrl; 7 | 8 | public SearchResult(String title, String description, String iconUrl) { 9 | this.title = title; 10 | this.description = description; 11 | this.iconUrl = iconUrl; 12 | } 13 | 14 | public String getTitle() { 15 | return title; 16 | } 17 | 18 | public void setTitle(String title) { 19 | this.title = title; 20 | } 21 | 22 | public String getDescription() { 23 | return description; 24 | } 25 | 26 | public void setDescription(String description) { 27 | this.description = description; 28 | } 29 | 30 | public String getIconUrl() { 31 | return iconUrl; 32 | } 33 | 34 | public void setIconUrl(String iconUrl) { 35 | this.iconUrl = iconUrl; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/SearchResultAdapter.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.ImageView; 8 | import android.widget.TextView; 9 | 10 | import java.util.Collection; 11 | import java.util.List; 12 | 13 | public class SearchResultAdapter extends RecyclerView.Adapter { 14 | private List mItemList; 15 | 16 | public SearchResultAdapter(List mItemList) { 17 | this.mItemList = mItemList; 18 | } 19 | 20 | @Override 21 | public SearchResultViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 22 | View v = LayoutInflater.from(parent.getContext()) 23 | .inflate(R.layout.layout_item_search_result, parent, false); 24 | return new SearchResultViewHolder(v); 25 | } 26 | 27 | @Override 28 | public void onBindViewHolder(SearchResultViewHolder holder, int position) { 29 | SearchResult result = mItemList.get(position); 30 | holder.mTitleTextView.setText(result.getTitle()); 31 | holder.mDescriptionTextView.setText(result.getDescription()); 32 | } 33 | 34 | @Override 35 | public int getItemCount() { 36 | return mItemList.size(); 37 | } 38 | 39 | public static class SearchResultViewHolder extends RecyclerView.ViewHolder { 40 | 41 | private TextView mTitleTextView; 42 | private TextView mDescriptionTextView; 43 | private ImageView mIconImageView; 44 | public SearchResultViewHolder(View itemView) { 45 | super(itemView); 46 | mTitleTextView = (TextView) itemView.findViewById(R.id.textview_title); 47 | mDescriptionTextView = (TextView) itemView.findViewById(R.id.textview_description); 48 | mIconImageView = (ImageView) itemView.findViewById(R.id.imageview_icon); 49 | 50 | } 51 | } 52 | 53 | 54 | 55 | public void addAll(Collection items) { 56 | int currentItemCount = mItemList.size(); 57 | mItemList.addAll(items); 58 | notifyItemRangeInserted(currentItemCount, items.size()); 59 | } 60 | 61 | public void addAll(int position, Collection items) { 62 | int currentItemCount = mItemList.size(); 63 | if(position > currentItemCount) 64 | throw new IndexOutOfBoundsException(); 65 | else 66 | mItemList.addAll(position, items); 67 | notifyItemRangeInserted(position, items.size()); 68 | } 69 | 70 | public void replaceWith(Collection items) { 71 | replaceWith(items, false); 72 | } 73 | 74 | public void clear() { 75 | int itemCount = mItemList.size(); 76 | mItemList.clear(); 77 | notifyItemRangeRemoved(0, itemCount); 78 | } 79 | 80 | 81 | public void replaceWith(Collection items, boolean cleanToReplace) { 82 | if(cleanToReplace) { 83 | clear(); 84 | addAll(items); 85 | } else { 86 | int oldCount = mItemList.size(); 87 | int newCount = items.size(); 88 | int delCount = oldCount - newCount; 89 | mItemList.clear(); 90 | mItemList.addAll(items); 91 | if(delCount > 0) { 92 | notifyItemRangeChanged(0, newCount); 93 | notifyItemRangeRemoved(newCount, delCount); 94 | } else if(delCount < 0) { 95 | notifyItemRangeChanged(0, oldCount); 96 | notifyItemRangeInserted(oldCount, - delCount); 97 | } else { 98 | notifyItemRangeChanged(0, newCount); 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /sample/src/main/java/org/cryse/widget/persistentsearch/sample/SimpleAnimationListener.java: -------------------------------------------------------------------------------- 1 | package org.cryse.widget.persistentsearch.sample; 2 | 3 | import android.animation.Animator; 4 | 5 | public class SimpleAnimationListener implements Animator.AnimatorListener { 6 | @Override 7 | public void onAnimationStart(Animator animation) { 8 | 9 | } 10 | 11 | @Override 12 | public void onAnimationEnd(Animator animation) { 13 | } 14 | 15 | @Override 16 | public void onAnimationCancel(Animator animation) { 17 | 18 | } 19 | 20 | @Override 21 | public void onAnimationRepeat(Animator animation) { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-hdpi/ic_action_search.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-hdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-hdpi/ic_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-mdpi/ic_action_search.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-mdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-mdpi/ic_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-xhdpi/ic_action_search.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xhdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-xhdpi/ic_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-xxhdpi/ic_action_search.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxhdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-xxhdpi/ic_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_action_search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-xxxhdpi/ic_action_search.png -------------------------------------------------------------------------------- /sample/src/main/res/drawable-xxxhdpi/ic_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crysehillmes/PersistentSearchView/c5fd1146b3383a5ed2182adf5b0beb5922ff3f2c/sample/src/main/res/drawable-xxxhdpi/ic_logo.png -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 17 | 18 |