├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── app
├── build.gradle
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── br
│ │ └── com
│ │ └── mauker
│ │ ├── MsvAuthority.kt
│ │ └── materialsearchview
│ │ └── app
│ │ └── MainActivity.kt
│ └── res
│ ├── layout
│ └── activity_main.xml
│ ├── menu
│ └── menu_main.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── values-pt-rBR
│ └── strings.xml
│ ├── values-w820dp
│ └── dimens.xml
│ └── values
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── library
├── build.gradle
├── proguard-android.txt
└── src
│ ├── androidTest
│ └── java
│ │ └── br
│ │ └── com
│ │ └── mauker
│ │ └── materialsearchview
│ │ └── HistoryProviderTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── br
│ │ └── com
│ │ └── mauker
│ │ └── materialsearchview
│ │ ├── MaterialSearchView.kt
│ │ ├── adapters
│ │ └── CursorSearchAdapter.kt
│ │ ├── db
│ │ ├── HistoryContract.kt
│ │ ├── HistoryDbHelper.kt
│ │ └── HistoryProvider.kt
│ │ └── utils
│ │ └── AnimationUtils.kt
│ └── res
│ ├── drawable
│ ├── ic_action_navigation_arrow_back.xml
│ ├── ic_action_navigation_close.xml
│ ├── ic_action_search_white.xml
│ ├── ic_action_voice_search.xml
│ └── ic_history_white.xml
│ ├── layout
│ ├── list_item.xml
│ └── search_view.xml
│ ├── values-bs
│ └── strings.xml
│ ├── values-es
│ └── strings.xml
│ ├── values-fr
│ └── strings.xml
│ ├── values-hr
│ └── strings.xml
│ ├── values-it
│ └── strings.xml
│ ├── values-nl
│ └── strings.xml
│ ├── values-pt-rBR
│ └── strings.xml
│ ├── values-sr
│ └── strings.xml
│ └── values
│ ├── attrs.xml
│ ├── colors.xml
│ ├── dimens.xml
│ └── strings.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/android,osx,windows,linux,intellij,java
2 |
3 | ### Android ###
4 | # Built application files
5 | *.apk
6 | *.ap_
7 |
8 | # Files for the Dalvik VM
9 | *.dex
10 |
11 | # Java class files
12 | *.class
13 |
14 | # Generated files
15 | bin/
16 | gen/
17 |
18 | # Gradle files
19 | .gradle/
20 | build/
21 | gradlew
22 | gradlew.bat
23 | gradle/wrapper/gradle-wrapper.properties
24 |
25 | # Local configuration file (sdk path, etc)
26 | local.properties
27 |
28 | # Proguard folder generated by Eclipse
29 | proguard/
30 |
31 | # Log Files
32 | *.log
33 |
34 | # Android Studio Navigation editor temp files
35 | .navigation/
36 |
37 | ### Android Patch ###
38 | gen-external-apklibs
39 |
40 |
41 | ### OSX ###
42 | .DS_Store
43 | .AppleDouble
44 | .LSOverride
45 |
46 | # Icon must end with two \r
47 | Icon
48 |
49 |
50 | # Thumbnails
51 | ._*
52 |
53 | # Files that might appear in the root of a volume
54 | .DocumentRevisions-V100
55 | .fseventsd
56 | .Spotlight-V100
57 | .TemporaryItems
58 | .Trashes
59 | .VolumeIcon.icns
60 |
61 | # Directories potentially created on remote AFP share
62 | .AppleDB
63 | .AppleDesktop
64 | Network Trash Folder
65 | Temporary Items
66 | .apdisk
67 |
68 |
69 | ### Windows ###
70 | # Windows image file caches
71 | Thumbs.db
72 | ehthumbs.db
73 |
74 | # Folder config file
75 | Desktop.ini
76 |
77 | # Recycle Bin used on file shares
78 | $RECYCLE.BIN/
79 |
80 | # Windows Installer files
81 | *.cab
82 | *.msi
83 | *.msm
84 | *.msp
85 |
86 | # Windows shortcuts
87 | *.lnk
88 |
89 |
90 | ### Linux ###
91 | *~
92 |
93 | # KDE directory preferences
94 | .directory
95 |
96 | # Linux trash folder which might appear on any partition or disk
97 | .Trash-*
98 |
99 |
100 | ### Intellij ###
101 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio
102 |
103 | *.iml
104 |
105 | ## Directory-based project format:
106 | .idea/
107 | # if you remove the above rule, at least ignore the following:
108 |
109 | # User-specific stuff:
110 | # .idea/workspace.xml
111 | # .idea/tasks.xml
112 | # .idea/dictionaries
113 |
114 | # Sensitive or high-churn files:
115 | # .idea/dataSources.ids
116 | # .idea/dataSources.xml
117 | # .idea/sqlDataSources.xml
118 | # .idea/dynamic.xml
119 | # .idea/uiDesigner.xml
120 |
121 | # Gradle:
122 | # .idea/gradle.xml
123 | # .idea/libraries
124 |
125 | # Mongo Explorer plugin:
126 | # .idea/mongoSettings.xml
127 |
128 | ## File-based project format:
129 | *.ipr
130 | *.iws
131 |
132 | ## Plugin-specific files:
133 |
134 | # IntelliJ
135 | /out/
136 |
137 | # mpeltonen/sbt-idea plugin
138 | .idea_modules/
139 |
140 | # JIRA plugin
141 | atlassian-ide-plugin.xml
142 |
143 | # Crashlytics plugin (for Android Studio and IntelliJ)
144 | com_crashlytics_export_strings.xml
145 | crashlytics.properties
146 | crashlytics-build.properties
147 |
148 |
149 | ### Java ###
150 | *.class
151 |
152 | # Mobile Tools for Java (J2ME)
153 | .mtj.tmp/
154 |
155 | # Package Files #
156 | *.jar
157 | *.war
158 | *.ear
159 |
160 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
161 | hs_err_pid*
162 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 |
3 | before_install:
4 | - yes | sdkmanager "platforms;android-30"
5 |
6 | android:
7 | components:
8 | - tools
9 | - platform-tools
10 | - build-tools-30.0.2
11 | - extra-android-m2repository
12 | - extra-android-support
13 | - android-30
14 |
15 | jdk:
16 | - oraclejdk8
17 |
18 | script:
19 | - chmod +x gradlew
20 | - ./gradlew clean build --stacktrace --info
21 |
22 |
23 | notifications:
24 | email: false
25 |
26 | sudo: false
27 |
28 | cache:
29 | directories:
30 | - $HOME/.gradle
31 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Change Log
2 | ==========
3 |
4 | ## Version 1.2.1
5 |
6 | _2017-03-20_
7 |
8 | * Fixed a bug where MSV was crashing on devices with Kitkat or older due to the use of Vector Drawables. For more info on this issue, check [issue #92](https://github.com/Mauker1/MaterialSearchView/issues/92).
9 |
10 | ## Version 1.2.1
11 |
12 | _2017-03-16_
13 |
14 | * You can now get the default adapter by calling `MaterialSearchView#getAdapter()` and have more control over it;
15 | * Added the `getCurrentQuery()` method, making it possible to get the query anywhere in the application;
16 | * Added the `setCloseOnTintClick(boolean)` method. If you set it to `true`, a touch outside the result list will close the `MaterialSearchView`, it'll remain open otherwise;
17 | * Added the `setSearchBarColor(int color)` method, where you can change specifically the search bar color;
18 | * The `saveQueryToDb()` method is now public, giving the programmer more control over when to save the queries;
19 | * The `setTintBackground(int color)` method is now private. It's been replaced by the public method `setBackgroundColor(int color)`;
20 | * Added French, Dutch, Bosnian, Croatian and Serbian translations.
21 |
22 | ## Version 1.2.0
23 |
24 | _2016-10-27_
25 |
26 | * Suggestions can now be inserted and removed one by one;
27 | * It's now possible to change the voice hint prompt and the searchBarHeight;
28 | * MSV now have RTL support;
29 | * MSV now is correctly displayed with transparent status bar;
30 | * `CoordinatorLayout` was dropped. Now `FrameLayout` is the new root for MSV;
31 | * Added italian translation;
32 | * Bunch of bug fixes (See [this link](https://github.com/Mauker1/MaterialSearchView/milestone/2) for more information).
33 |
34 | ## Version 1.1.3
35 |
36 | _2016-08-14_
37 |
38 | * Fixed a bug where `onQueryTextChanged` interface method was called multiple times;
39 | * Added `OnItemLongClickListener` on history/suggestion list;
40 | * Implemented an `OnClickListener` to the voice search icon, so it's possible to change the click behavior.
41 |
42 | ## Version 1.1.2
43 |
44 | _2016-07-19_
45 |
46 | * Now it's possible to change the Search View input type.
47 | * Added the `getItemAtPosition()` method to simplify how you get the list item String.
48 |
49 | **e.g.:** `search_view.setInputType(InputType.TYPE_CLASS_TEXT);`
50 |
51 | ## Version 1.1.1
52 |
53 | _2016-06-23_
54 |
55 | * Just some bug fixes.
56 |
57 | ## Version 1.1.0
58 |
59 | _2016-05-08_
60 |
61 | * Solved the issue with the Content Provider authority, now it's possible to install multiple instances of this lib (Fix #7).
62 | * Added support for a transparent suggestion list.
63 | * Added support for styles, so now it's possible to change some of the look and feel of the Search View.
64 | * Now it's possible to change how many search history results the view will show.
65 |
66 | **Note:** To get the library to work, now you have to implement a class
67 | named `MsvAuthority` inside the `br.com.mauker` package, and it should
68 | have a public static String variable called `CONTENT_AUTHORITY`.
69 | Give it the value you want and don't forget to add the same name on your
70 | manifest file.
71 |
72 | More information, on [this link](http://stackoverflow.com/a/14592121/4070469).
73 |
74 | ## Version 1.0.3
75 |
76 | _2016-04-21_
77 |
78 | Initial release.
79 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2016 Maurício Pessoa
2 |
3 | Licensed under the Apache License, Version 2.0 (the "License");
4 | you may not use this file except in compliance with the License.
5 | You may obtain a copy of the License at
6 |
7 | http://www.apache.org/licenses/LICENSE-2.0
8 |
9 | Unless required by applicable law or agreed to in writing, software
10 | distributed under the License is distributed on an "AS IS" BASIS,
11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | See the License for the specific language governing permissions and
13 | limitations under the License.
14 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MaterialSearchView
2 | Android SearchView based on Material Design guidelines. The MaterialSearchView will overlay a Toolbar or ActionBar as well as display a ListView for the user to show suggested or recent searches.
3 |
4 | Stable: [](https://bintray.com/mauker/maven/MaterialSearchView/1.3.0-rc02)
5 | Beta: [](https://bintray.com/mauker/maven/MaterialSearchView/_latestVersion)
6 |
7 | [](https://travis-ci.org/Mauker1/MaterialSearchView)
8 | 
9 |
10 |
11 |
12 | [](https://android-arsenal.com/details/1/3469)
13 | [](https://github.com/Mauker1/MaterialSearchView/blob/master/LICENSE)
14 |
15 |
16 |
17 | ## Download
18 | To add the MaterialSearchView library to your Android Studio project, simply add the following gradle dependency:
19 | ```java
20 | implementation 'br.com.mauker.materialsearchview:materialsearchview:1.3.0-rc02'
21 | ```
22 |
23 | This library is supported with a min SDK of 14.
24 |
25 | **Important note:** If you're still using version 1.0.3, it's recommended to upgrade to the latest version as soon as possible. For more information, please see [this issue](https://github.com/Mauker1/MaterialSearchView/issues/7).
26 |
27 | **New version note**: MSV 2.0 is now on beta stage, if you wish to test it, get it by using:
28 |
29 | ```java
30 | implementation 'br.com.mauker.materialsearchview:materialsearchview:2.0.0-beta02'
31 | ```
32 |
33 | **Version 2.0 doesn't require the Content Provider setup** and had some API changes which will be added to the documentation later on. For more details please take a look at the [V_2.0 branch](https://github.com/Mauker1/MaterialSearchView/tree/milestone/2_0).
34 |
35 | **Important note on V 2.0:** Since I'm using Coroutine Actors, which is marked as obsolete, you'll get a lint warning on each class that uses MSV, to get rid of those add this to your app `build.gradle` file:
36 |
37 | ```
38 | kotlinOptions.freeCompilerArgs += [
39 | "-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
40 | "-Xuse-experimental=kotlinx.coroutines.ObsoleteCoroutinesApi"
41 | ]
42 | ```
43 |
44 | Once the Actors methods are updated, I'll update the lib as well.
45 |
46 | ## Setup
47 |
48 | Before you can use this lib, you have to implement a class named `MsvAuthority` inside the `br.com.mauker` package on your app module, and it should have a public static String variable called `CONTENT_AUTHORITY`. Give it the value you want and **don't forget** to add the same name on your manifest file. The lib will use this file to set the Content Provider authority.
49 |
50 | **Example:**
51 |
52 | **MsvAuthority.java**
53 |
54 | ```java
55 | package br.com.mauker;
56 |
57 | public class MsvAuthority {
58 | public static final String CONTENT_AUTHORITY = "br.com.mauker.materialsearchview.searchhistorydatabase";
59 | }
60 | ```
61 |
62 | Or if you're using Kotlin:
63 |
64 | **MsvAuthority.kt**
65 | ```Kotlin
66 | package br.com.mauker
67 |
68 | object MsvAuthority {
69 | const val CONTENT_AUTHORITY: String = "br.com.mauker.materialsearchview.searchhistorydatabase"
70 | }
71 | ```
72 |
73 | **AndroidManifest.xml**
74 |
75 | ```xml
76 |
77 |
78 |
79 |
80 |
86 |
87 |
88 |
89 | ```
90 |
91 | **Proguard note:** Some of you might experience some problems with Proguard deleting the authority class, to solve those problems, add the following lines on your proguard file:
92 |
93 | ```
94 | -keep class br.com.mauker.MsvAuthority
95 | -keepclassmembers class br.com.mauker.** { *; }
96 | ```
97 |
98 | ## Usage
99 |
100 | To open the search view on your app, add the following code **to the end of your layout**:
101 |
102 | ```xml
103 |
107 | ```
108 |
109 | Then, inside your `Activity` get the reference:
110 |
111 | ```kotlin
112 | // Activity:
113 | val searchView: MaterialSearchView = findViewById(R.id.search_view)
114 | ```
115 |
116 | - To open the search view, simply call the `searchView.openSearch()` method.
117 |
118 | - To close the search view, call the `searchView.closeSearch()` method.
119 |
120 | - You can check if the view is open by using the `searchView.isOpen()` method.
121 |
122 | - As from Version 1.2.1 it's also possible to get the query anytime by using the `searchView.getCurrentQuery()` method.
123 |
124 | - To close the search view using the back button, put the following code on your `Activity`:
125 |
126 | ```kotlin
127 | override fun onBackPressed() {
128 | if (searchView.isOpen) {
129 | // Close the search on the back button press.
130 | searchView.closeSearch()
131 | } else {
132 | super.onBackPressed()
133 | }
134 | }
135 | ```
136 |
137 | For more examples on how to use this lib, [check the sample app code here](https://github.com/Mauker1/MaterialSearchView/blob/master/app/src/main/java/br/com/mauker/materialsearchview/app/MainActivity.kt).
138 |
139 | ## Search history and suggestions
140 |
141 | You can provide search suggestions by using the following methods:
142 |
143 | - `addSuggestions(suggestions: Array)`
144 | - `addSuggestions(suggestions: List)`
145 |
146 | It's also possible to add a single suggestion using the following method:
147 |
148 | - `addSuggestion(suggestion: String?)`
149 |
150 | To remove all the search suggestions use:
151 |
152 | - `clearSuggestions()`
153 |
154 | And to remove a single suggestion, use the following method:
155 |
156 | - `removeSuggestion(suggestion: String?)`
157 |
158 | The search history is automatically handled by the view, and it can be cleared by using:
159 |
160 | - `clearHistory()`
161 |
162 | You can also remove both by using the method below:
163 |
164 | - `clearAll()`
165 |
166 | ## Modifying the suggestion list behavior
167 |
168 | The suggestion list is based on a `ListView`, and as such you can define the behavior of the item click by using the `MaterialSearchView#setOnItemClickListener()` method.
169 |
170 | If you want to submit the query from the selected suggestion, you can use the snippet below:
171 |
172 | ```kotlin
173 | searchView.setOnItemClickListener { _, _, position, _ ->
174 | // Do something when the suggestion list is clicked.
175 | val suggestion = searchView.getSuggestionAtPosition(position)
176 | searchView.setQuery(suggestion, false)
177 | }
178 | ```
179 |
180 | If you just want to set the text on the search view text field when the user selects the suggestion, change the second argument from the `searchView#setQuery()` from `true` to `false`.
181 |
182 | ## Styling the View
183 |
184 | You can change how your MaterialSearchView looks like. To achieve that effect, try to add the following lines to your styles.xml:
185 |
186 | ```xml
187 |
204 |
205 | ```
206 |
207 | Alternatively, you can also style the Search View programmatically by calling the methods:
208 |
209 | - `setBackgroundColor(int color);`
210 | - `setTintAlpha(int alpha);`
211 | - `setSearchBarColor(int color);`
212 | - `setSearchBarHeight(int height);`
213 | - `setTextColor(int color);`
214 | - `setHintTextColor(int color);`
215 | - `setHint(String hint);`
216 | - `setVoiceHintPrompt(String voiceHint);`
217 | - `setVoiceIcon(DrawableRes int resourceId);`
218 | - `setClearIcon(DrawableRes int resourceId);`
219 | - `setBackIcon(DrawableRes int resourceId);`
220 | - `setSuggestionBackground(DrawableRes int resourceId);`
221 | - `setHistoryIcon(@DrawableRes int resourceId);`
222 | - `setSuggestionIcon(@DrawableRes int resourceId);`
223 | - `setListTextColor(int color);`
224 |
225 | And add this line on your `br.com.mauker.materialsearchview.MaterialSearchView` tag:
226 |
227 | ```xml
228 | style="@style/MaterialSearchViewStyle"
229 | ```
230 |
231 | So it'll look like:
232 |
233 | ```xml
234 |
239 | ```
240 |
241 | ## Interfaces
242 | Currently there are two interfaces that you can use to instantiate listeners for:
243 |
244 | - `OnQueryTextListener`: Use this interface to handle QueryTextChange or QueryTextSubmit events inside the MaterialSearchView.
245 | - `SearchViewListener`: You can use this interface to listen and handle the open or close events of the MaterialSearchView.
246 |
247 |
248 | ## Languages
249 |
250 | The MaterialSearchView supports the following languages:
251 |
252 | - English (en_US);
253 | - Brazillian Portuguese (pt_BR);
254 | - Italian (Thanks to [Francesco Donzello](https://github.com/wideawake));
255 | - French (Thanks to [Robin](https://github.com/RobinPetit));
256 | - Bosnian, Croatian and Serbian (Thanks to [Luke](https://github.com/luq-0));
257 | - Spanish (Thanks to [Gloix](https://github.com/Gloix)).
258 |
259 | ## Sample GIF
260 |
261 |
262 | ## More Info
263 |
264 | For more use cases, and some examples, you can [check the sample app](https://github.com/Mauker1/MaterialSearchView/tree/master/app/src/main/java/br/com/mauker/materialsearchview/app).
265 |
266 | ## Credits
267 | This library was created by Maurício Pessoa with contributions from:
268 | - [Adam McNeilly](http://adammcneilly.com)
269 | - [Pier Betos](https://github.com/peterbetos)
270 |
271 | JCenter version was made possible with help from:
272 |
273 | - [Eric Cugota](https://github.com/tryadelion)
274 |
275 | This project was inspired by the [MaterialSearchView](https://github.com/krishnakapil/MaterialSeachView) library by krishnakapil.
276 |
277 | ## License
278 | The MaterialSearchView library is available under the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0).
279 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | def globalConfiguration = rootProject.extensions.getByName("ext")
6 |
7 | compileSdkVersion globalConfiguration["androidCompileSdkVersion"]
8 | buildToolsVersion globalConfiguration["androidBuildToolsVersion"]
9 |
10 | defaultConfig {
11 | // vectorDrawables.useSupportLibrary = true
12 | applicationId "br.com.mauker.materialsearchview.app"
13 | minSdkVersion globalConfiguration["androidMinSdkVersion"]
14 | targetSdkVersion globalConfiguration["androidTargetSdkVersion"]
15 | versionCode 4
16 | versionName "1.0"
17 | }
18 | buildTypes {
19 | release {
20 | minifyEnabled false
21 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22 | }
23 | }
24 |
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_1_8
27 | targetCompatibility JavaVersion.VERSION_1_8
28 | }
29 |
30 | useLibrary 'android.test.base'
31 | }
32 |
33 | dependencies {
34 | implementation fileTree(dir: 'libs', include: ['*.jar'])
35 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
36 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
37 | implementation project(':library')
38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
39 | }
40 | repositories {
41 | mavenCentral()
42 | }
43 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
28 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/java/br/com/mauker/MsvAuthority.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker
2 |
3 | /**
4 | * Class used to setup the MaterialSearchView history database.
5 | */
6 | object MsvAuthority {
7 | const val CONTENT_AUTHORITY: String = "br.com.mauker.materialsearchview.searchhistorydatabase"
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/br/com/mauker/materialsearchview/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview.app
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.os.Bundle
6 | import android.speech.RecognizerIntent
7 | import android.text.TextUtils
8 | import android.view.Menu
9 | import android.view.MenuItem
10 | import android.widget.Button
11 | import android.widget.Toast
12 | import androidx.appcompat.app.AppCompatActivity
13 | import androidx.appcompat.widget.Toolbar
14 | import br.com.mauker.materialsearchview.MaterialSearchView
15 | import br.com.mauker.materialsearchview.MaterialSearchView.SearchViewListener
16 |
17 | class MainActivity : AppCompatActivity() {
18 | private lateinit var searchView: MaterialSearchView
19 | private lateinit var btClearHistory: Button
20 | private lateinit var btClearSuggestions: Button
21 | private lateinit var btClearAll: Button
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 | setContentView(R.layout.activity_main)
26 |
27 | val toolbar: Toolbar = findViewById(R.id.toolbar)
28 | setSupportActionBar(toolbar)
29 |
30 | searchView = findViewById(R.id.search_view)
31 | btClearHistory = findViewById(R.id.bt_clearHistory)
32 | btClearSuggestions = findViewById(R.id.bt_clearSuggestions)
33 | btClearAll = findViewById(R.id.bt_clearAll)
34 |
35 | searchView.setOnQueryTextListener(object : MaterialSearchView.OnQueryTextListener {
36 | override fun onQueryTextSubmit(query: String): Boolean {
37 | return false
38 | }
39 |
40 | override fun onQueryTextChange(newText: String): Boolean {
41 | return false
42 | }
43 | })
44 |
45 | searchView.setSearchViewListener(object : SearchViewListener {
46 | override fun onSearchViewOpened() {
47 | // Do something once the view is open.
48 | }
49 |
50 | override fun onSearchViewClosed() {
51 | // Do something once the view is closed.
52 | }
53 | })
54 | searchView.setOnItemClickListener { _, _, position, _ -> // Do something when the suggestion list is clicked.
55 | val suggestion = searchView.getSuggestionAtPosition(position)
56 | searchView.setQuery(suggestion, false)
57 | }
58 | searchView.setOnClearClickListener {
59 | Toast.makeText(this, "Clear clicked!", Toast.LENGTH_LONG).show()
60 | }
61 | btClearHistory.setOnClickListener { clearHistory() }
62 | btClearSuggestions.setOnClickListener { clearSuggestions() }
63 | btClearAll.setOnClickListener { clearAll() }
64 |
65 | searchView.adjustTintAlpha(0.8f)
66 | val context: Context = this
67 | searchView.setOnItemLongClickListener { _, _, i, _ ->
68 | Toast.makeText(context, "Long clicked position: $i", Toast.LENGTH_SHORT).show()
69 | true
70 | }
71 | // This will override the default audio action.
72 | searchView.setOnVoiceClickedListener { Toast.makeText(context, "Voice clicked!", Toast.LENGTH_SHORT).show() }
73 | }
74 |
75 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
76 | // Inflate the menu; this adds items to the action bar if it is present.
77 | menuInflater.inflate(R.menu.menu_main, menu)
78 | return true
79 | }
80 |
81 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
82 | // Handle toolbar item clicks here. It'll
83 | // automatically handle clicks on the Home/Up button, so long
84 | // as you specify a parent activity in AndroidManifest.xml.
85 | when (item.itemId) {
86 | R.id.action_search -> {
87 | // Open the search view on the menu item click.
88 | searchView.openSearch()
89 | return true
90 | }
91 | }
92 | return super.onOptionsItemSelected(item)
93 | }
94 |
95 | override fun onBackPressed() {
96 | if (searchView.isOpen) {
97 | // Close the search on the back button press.
98 | searchView.closeSearch()
99 | } else {
100 | super.onBackPressed()
101 | }
102 | }
103 |
104 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
105 | if (requestCode == MaterialSearchView.REQUEST_VOICE && resultCode == RESULT_OK) {
106 | val matches = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
107 | if (matches != null && matches.size > 0) {
108 | val searchWrd = matches[0]
109 | if (!TextUtils.isEmpty(searchWrd)) {
110 | searchView.setQuery(searchWrd, false)
111 | }
112 | }
113 | return
114 | }
115 | super.onActivityResult(requestCode, resultCode, data)
116 | }
117 |
118 | override fun onPause() {
119 | super.onPause()
120 | searchView.clearSuggestions()
121 | }
122 |
123 | override fun onResume() {
124 | super.onResume()
125 | searchView.activityResumed()
126 | val arr = resources.getStringArray(R.array.suggestions)
127 | searchView.addSuggestions(arr)
128 | }
129 |
130 | private fun clearHistory() {
131 | searchView.clearHistory()
132 | }
133 |
134 | private fun clearSuggestions() {
135 | searchView.clearSuggestions()
136 | }
137 |
138 | private fun clearAll() {
139 | searchView.clearAll()
140 | }
141 | }
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
19 |
20 |
28 |
29 |
37 |
38 |
46 |
47 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mauker1/MaterialSearchView/ca4e88788a9eca3dac86bcb1d445794700023192/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mauker1/MaterialSearchView/ca4e88788a9eca3dac86bcb1d445794700023192/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mauker1/MaterialSearchView/ca4e88788a9eca3dac86bcb1d445794700023192/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mauker1/MaterialSearchView/ca4e88788a9eca3dac86bcb1d445794700023192/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | MaterialSearch
4 |
5 | Hello world!
6 | Settings
7 |
8 | Limpar histórico
9 | Limpar sugestões
10 | Limpar tudo
11 |
12 |
13 | - Maçã
14 | - Damasco
15 | - Abacate
16 | - Banana
17 | - Mirtilo
18 | - Amora preta
19 | - Cenoura
20 | - Cereja
21 | - Laranja
22 | - Batata
23 | - Pêssego
24 | - Maracujá
25 | - Pêra
26 | - Morango
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | MaterialSearch
3 |
4 | Hello world!
5 | Settings
6 |
7 | Clear history
8 | Clear suggestions
9 | Clear all
10 |
11 |
12 | - Apple
13 | - Apricot
14 | - Avocado
15 | - Banana
16 | - Blueberry
17 | - Blackberry
18 | - Carrot
19 | - Cherry
20 | - Orange
21 | - Potato
22 | - Peach
23 | - Passion fruit
24 | - Pear
25 | - Strawberry
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext.kotlin_version = '1.4.21'
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 | dependencies {
10 | classpath 'com.android.tools.build:gradle:4.1.2'
11 | classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7'
12 | classpath 'com.github.dcendents:android-maven-plugin:1.2'
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14 |
15 | // NOTE: Do not place your application dependencies here; they belong
16 | // in the individual module build.gradle files
17 | }
18 | }
19 |
20 | allprojects {
21 | repositories {
22 | google()
23 | jcenter()
24 | }
25 |
26 | ext {
27 | androidBuildToolsVersion = "30.0.2"
28 | androidMinSdkVersion = 14
29 | androidTargetSdkVersion = 30
30 | androidCompileSdkVersion = 30
31 |
32 | appCompatVersion = "1.2.0"
33 | constraintLayoutVersion = "2.0.0"
34 | materialDesignVersion = "1.2.1"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | ## Project-wide Gradle settings.
2 | #
3 | # For more details on how to configure your build environment visit
4 | # http://www.gradle.org/docs/current/userguide/build_environment.html
5 | #
6 | # The Gradle daemon aims to improve the startup and execution time of Gradle.
7 | # When set to true the Gradle daemon is to run the build.
8 | org.gradle.daemon=true
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 | #
20 | # Enables new incubating mode that makes Gradle selective when configuring projects.
21 | # Only relevant projects are configured which results in faster builds for large multi-projects.
22 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:configuration_on_demand
23 | org.gradle.configureondemand=true
24 | android.useAndroidX=true
25 | android.enableJetifier=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mauker1/MaterialSearchView/ca4e88788a9eca3dac86bcb1d445794700023192/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Jan 18 01:54:45 BRT 2021
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-6.8.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # 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 |
--------------------------------------------------------------------------------
/library/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.library'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 |
5 | android {
6 | def globalConfiguration = rootProject.extensions.getByName("ext")
7 |
8 | compileSdkVersion globalConfiguration["androidCompileSdkVersion"]
9 | buildToolsVersion globalConfiguration["androidBuildToolsVersion"]
10 |
11 | defaultConfig {
12 | minSdkVersion globalConfiguration["androidMinSdkVersion"]
13 | targetSdkVersion globalConfiguration["androidTargetSdkVersion"]
14 | versionCode 16
15 | versionName "1.3.0-rc02"
16 |
17 | vectorDrawables.useSupportLibrary = true
18 |
19 | buildConfigField "int", "MAX_HISTORY", "10"
20 | consumerProguardFiles 'proguard-android.txt'
21 |
22 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23 | }
24 | buildTypes {
25 | release {
26 | minifyEnabled false
27 | // proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
28 | }
29 | }
30 |
31 | lintOptions {
32 | abortOnError false
33 | }
34 |
35 | compileOptions {
36 | sourceCompatibility = 1.8
37 | targetCompatibility = 1.8
38 | }
39 |
40 | kotlinOptions {
41 | jvmTarget = "1.8"
42 | }
43 |
44 | useLibrary 'android.test.runner'
45 | useLibrary 'android.test.base'
46 | useLibrary 'android.test.mock'
47 | }
48 |
49 | dependencies {
50 | implementation fileTree(dir: 'libs', include: ['*.jar'])
51 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
52 | implementation "com.google.android.material:material:$materialDesignVersion"
53 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
54 |
55 | androidTestImplementation 'androidx.test:runner:1.3.0'
56 | androidTestImplementation 'androidx.test:rules:1.3.0'
57 | // Assertions
58 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
59 | androidTestImplementation 'androidx.test.ext:truth:1.3.0'
60 | androidTestImplementation 'com.google.truth:truth:1.0'
61 | // Espresso
62 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
63 | }
64 |
65 | ext {
66 | PUBLISH_GROUP_ID = 'br.com.mauker.materialsearchview'
67 | PUBLISH_ARTIFACT_ID = 'materialsearchview'
68 | PUBLISH_VERSION = '1.3.0-rc02'
69 | }
70 |
71 | apply from: 'https://raw.githubusercontent.com/blundell/release-android-library/master/android-release-aar.gradle'
72 |
--------------------------------------------------------------------------------
/library/proguard-android.txt:
--------------------------------------------------------------------------------
1 | -keep class br.com.mauker.MsvAuthority
2 | -keepclassmembers class br.com.mauker.** { *; }
--------------------------------------------------------------------------------
/library/src/androidTest/java/br/com/mauker/materialsearchview/HistoryProviderTest.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview
2 |
3 | import androidx.test.ext.junit.runners.AndroidJUnit4
4 | import org.junit.runner.RunWith
5 |
6 | /**
7 | * Created by mauker on 15/04/16.
8 | *
9 | * Tests the HistoryProvider.
10 | *
11 | */
12 | @RunWith(AndroidJUnit4::class)
13 | class HistoryProviderTest {
14 | //
15 | // companion object {
16 | // private const val TEST_QUERY_01 = "Batata"
17 | // private const val TEST_QUERY_02 = "Banana"
18 | // private var time_01: Long = 0
19 | // private var time_02: Long = 0
20 | // private const val isHistory_01 = 0
21 | // private const val isHistory_02 = 1
22 | // }
23 | //
24 | // private val mContext = InstrumentationRegistry.getInstrumentation().targetContext
25 | //
26 | // @Before
27 | // @Throws(Exception::class)
28 | // fun setUp() {
29 | // testDeleteAllRecords()
30 | // time_01 = System.currentTimeMillis()
31 | // time_02 = time_01 + 1000
32 | // }
33 | //
34 | // @After
35 | // @Throws(Exception::class)
36 | // fun tearDown() {
37 | // testDeleteAllRecords()
38 | // }
39 | //
40 | // @Test
41 | // fun testDeleteAllRecords() {
42 | // // Kill them all.
43 | // mContext.contentResolver.delete(
44 | // HistoryContract.HistoryEntry.CONTENT_URI,
45 | // null,
46 | // null
47 | // )
48 | //
49 | // // And make sure they are dead.
50 | // val cursor = mContext.contentResolver.query(
51 | // HistoryContract.HistoryEntry.CONTENT_URI,
52 | // null,
53 | // null,
54 | // null,
55 | // null
56 | // )
57 | // Assert.assertNotNull(cursor)
58 | // Assert.assertEquals(0, cursor!!.count)
59 | // cursor.close()
60 | // }
61 | //
62 | // @Test
63 | // fun testGetType() {
64 | // // content-authority = br.com.mauker.searchhistorydatabase
65 | //
66 | // // Search History
67 | // var type = mContext.contentResolver.getType(HistoryContract.HistoryEntry.CONTENT_URI)
68 | // // vnd.android.cursor.dir/br.com.mauker.searchhistorydatabase/history
69 | // Assert.assertEquals(HistoryContract.HistoryEntry.CONTENT_TYPE, type)
70 | //
71 | // // Search History Date
72 | // // Content-authority + history/date
73 | // type = mContext.contentResolver.getType(buildHistoryUri(1))
74 | // // vnd.android.cursor.item/br.com.mauker.searchhistorydatabase/history/0
75 | // Assert.assertEquals(HistoryContract.HistoryEntry.CONTENT_ITEM, type)
76 | // }
77 | //
78 | // @Test
79 | // fun testInsertAndQuery() {
80 | // val values = historyContentValues
81 | // val historyInsertUri = mContext.contentResolver
82 | // .insert(HistoryContract.HistoryEntry.CONTENT_URI, values)
83 | // val id = ContentUris.parseId(historyInsertUri!!)
84 | //
85 | // // Check if it was inserted.
86 | // Assert.assertTrue(id > 0)
87 | //
88 | // // Query for all rows and validate cursor
89 | // val historyCursor = mContext.contentResolver.query(
90 | // HistoryContract.HistoryEntry.CONTENT_URI,
91 | // null,
92 | // null,
93 | // null,
94 | // null
95 | // )
96 | // Assert.assertNotNull(historyCursor)
97 | // validateCursor(historyCursor, values)
98 | // historyCursor!!.close()
99 | // }
100 | //
101 | // @Test
102 | // fun testUpdateHistory() {
103 | // // Insert the data first. And there's no need to test this.
104 | // // The insert test asserts that this works.
105 | // val values = historyContentValues
106 | // val historyInsertUri = mContext.contentResolver
107 | // .insert(HistoryContract.HistoryEntry.CONTENT_URI, values)
108 | // val id = ContentUris.parseId(historyInsertUri!!)
109 | // Assert.assertTrue(id > 0)
110 | //
111 | // // Now, let's update it with new values.
112 | // val newValues = historyContentValues02
113 | // newValues.put(HistoryContract.HistoryEntry._ID, id)
114 | // mContext.contentResolver.update(
115 | // HistoryContract.HistoryEntry.CONTENT_URI,
116 | // newValues,
117 | // HistoryContract.HistoryEntry._ID + " = ?", arrayOf(id.toString()))
118 | // val cursor = mContext.contentResolver.query(
119 | // buildHistoryUri(id),
120 | // null,
121 | // null,
122 | // null,
123 | // null
124 | // )
125 | // Assert.assertNotNull(cursor)
126 | // validateCursor(cursor, newValues)
127 | // cursor!!.close()
128 | // }
129 | //
130 | // private val historyContentValues: ContentValues
131 | // get() {
132 | // val values = ContentValues()
133 | // values.put(HistoryContract.HistoryEntry.COLUMN_QUERY, TEST_QUERY_01)
134 | // values.put(HistoryContract.HistoryEntry.COLUMN_INSERT_DATE, time_01)
135 | // values.put(HistoryContract.HistoryEntry.COLUMN_IS_HISTORY, isHistory_01)
136 | // return values
137 | // }
138 | // private val historyContentValues02: ContentValues
139 | // get() {
140 | // val values = ContentValues()
141 | // values.put(HistoryContract.HistoryEntry.COLUMN_QUERY, TEST_QUERY_02)
142 | // values.put(HistoryContract.HistoryEntry.COLUMN_INSERT_DATE, time_02)
143 | // values.put(HistoryContract.HistoryEntry.COLUMN_IS_HISTORY, isHistory_02)
144 | // return values
145 | // }
146 | //
147 | // private fun validateCursor(valueCursor: Cursor?, expectedValues: ContentValues) {
148 | // Assert.assertTrue(valueCursor!!.moveToFirst())
149 | // val valueSet = expectedValues.valueSet()
150 | // for ((columnName, value) in valueSet) {
151 | // val idx = valueCursor.getColumnIndex(columnName)
152 | // Assert.assertFalse(idx == -1)
153 | // when (valueCursor.getType(idx)) {
154 | // Cursor.FIELD_TYPE_FLOAT -> Assert.assertEquals(value, valueCursor.getDouble(idx))
155 | // Cursor.FIELD_TYPE_INTEGER -> Assert.assertEquals(value.toString().toLong(), valueCursor.getLong(idx))
156 | // Cursor.FIELD_TYPE_STRING -> Assert.assertEquals(value, valueCursor.getString(idx))
157 | // else -> Assert.assertEquals(value.toString(), valueCursor.getString(idx))
158 | // }
159 | // }
160 | // valueCursor.close()
161 | // }
162 | }
163 |
--------------------------------------------------------------------------------
/library/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/java/br/com/mauker/materialsearchview/MaterialSearchView.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview
2 |
3 | import android.animation.Animator
4 | import android.animation.AnimatorListenerAdapter
5 | import android.app.Activity
6 | import android.content.ContentValues
7 | import android.content.Context
8 | import android.content.Intent
9 | import android.content.res.Configuration
10 | import android.database.Cursor
11 | import android.graphics.Color
12 | import android.graphics.Rect
13 | import android.graphics.drawable.ColorDrawable
14 | import android.graphics.drawable.Drawable
15 | import android.os.Build
16 | import android.speech.RecognizerIntent
17 | import android.text.Editable
18 | import android.text.InputType
19 | import android.text.TextUtils
20 | import android.text.TextWatcher
21 | import android.util.AttributeSet
22 | import android.util.TypedValue
23 | import android.view.LayoutInflater
24 | import android.view.View
25 | import android.view.View.OnFocusChangeListener
26 | import android.view.inputmethod.InputMethodManager
27 | import android.widget.*
28 | import android.widget.AdapterView.OnItemClickListener
29 | import android.widget.AdapterView.OnItemLongClickListener
30 | import androidx.annotation.DrawableRes
31 | import androidx.appcompat.app.AppCompatDelegate
32 | import androidx.core.content.ContextCompat
33 | import br.com.mauker.materialsearchview.adapters.CursorSearchAdapter
34 | import br.com.mauker.materialsearchview.db.HistoryContract
35 | import br.com.mauker.materialsearchview.utils.AnimationUtils.circleHideView
36 | import br.com.mauker.materialsearchview.utils.AnimationUtils.circleRevealView
37 | import br.com.mauker.materialsearchview.utils.AnimationUtils.fadeInView
38 | import br.com.mauker.materialsearchview.utils.AnimationUtils.fadeOutView
39 | import java.util.*
40 | import kotlin.math.roundToInt
41 |
42 | /**
43 | * Created by Mauker and Adam McNeilly on 30/03/2016. dd/MM/YY.
44 | * Maintained by Mauker, Adam McNeilly and our beautiful open source community <3
45 | * Based on stadiko on 6/8/15. https://github.com/krishnakapil/MaterialSeachView
46 | */
47 | class MaterialSearchView @JvmOverloads constructor(
48 | private val mContext: Context, attributeSet: AttributeSet? = null, defStyleAttributes: Int = 0
49 | ) : FrameLayout(mContext, attributeSet) {
50 |
51 | companion object {
52 | //region Properties
53 | /**
54 | * The freaking log tag. Used for logs, duh.
55 | */
56 | private val LOG_TAG = MaterialSearchView::class.java.simpleName
57 |
58 | /**
59 | * The maximum number of results we want to return from the voice recognition.
60 | */
61 | private const val MAX_RESULTS = 1
62 |
63 | /**
64 | * The identifier for the voice request intent. (Guess why it's 42).
65 | */
66 | const val REQUEST_VOICE = 42
67 |
68 | /**
69 | * Number of suggestions to show.
70 | */
71 | private var MAX_HISTORY = BuildConfig.MAX_HISTORY
72 |
73 | private val EMPTY_STRING = ""
74 |
75 | /**
76 | * Sets how many items you want to show from the history database.
77 | *
78 | * @param maxHistory - The number of items you want to display.
79 | */
80 | fun setMaxHistoryResults(maxHistory: Int) {
81 | MAX_HISTORY = maxHistory
82 | }
83 | }
84 |
85 | //endregion
86 | //region Constructors
87 | init {
88 | // Initialize view
89 | init()
90 |
91 | // Initialize style
92 | initStyle(attributeSet, defStyleAttributes)
93 | }
94 |
95 | /**
96 | * Determines if the search view is opened or closed.
97 | * @return True if the search view is open, false if it is closed.
98 | */
99 | /**
100 | * Whether or not the search view is open right now.
101 | */
102 | var isOpen = false
103 | private set
104 |
105 | /**
106 | * Whether or not the MaterialSearchView will animate into view or just appear.
107 | */
108 | private var mShouldAnimate = true
109 |
110 | /**
111 | * Whether or not the MaterialSearchView will clonse under a click on the Tint View (Blank Area).
112 | */
113 | private var mShouldCloseOnTintClick = false
114 |
115 | /**
116 | * Wheter to keep the search history or not.
117 | */
118 | private var mShouldKeepHistory = true
119 |
120 | /**
121 | * Flag for whether or not we are clearing focus.
122 | */
123 | private var mClearingFocus = false
124 |
125 | /**
126 | * Voice hint prompt text.
127 | */
128 | private lateinit var mHintPrompt: String
129 |
130 | /**
131 | * Allows user to decide whether to allow voice search.
132 | */
133 | var isVoiceIconEnabled = false
134 | //endregion
135 |
136 | //region UI Elements
137 | /**
138 | * The tint that appears over the search view.
139 | */
140 | private lateinit var mTintView: View
141 |
142 | /**
143 | * The root of the search view.
144 | */
145 | private lateinit var mRoot: FrameLayout
146 |
147 | /**
148 | * The bar at the top of the SearchView containing the EditText and ImageButtons.
149 | */
150 | private lateinit var mSearchBar: LinearLayout
151 |
152 | /**
153 | * The EditText for entering a search.
154 | */
155 | private lateinit var mSearchEditText: EditText
156 |
157 | /**
158 | * The ImageButton for navigating back.
159 | */
160 | private lateinit var mBack: ImageButton
161 |
162 | /**
163 | * The ImageButton for initiating a voice search.
164 | */
165 | private lateinit var mVoice: ImageButton
166 |
167 | /**
168 | * The ImageButton for clearing the search text.
169 | */
170 | private lateinit var mClear: ImageButton
171 |
172 | /**
173 | * The ListView for displaying suggestions based on the search.
174 | */
175 | private lateinit var mSuggestionsListView: ListView
176 |
177 | /**
178 | * Adapter for displaying suggestions.
179 | */
180 | lateinit var adapter: CursorAdapter
181 | private set
182 | //endregion
183 |
184 | //region Query Properties
185 | /**
186 | * The previous query text.
187 | */
188 | private lateinit var mOldQuery: CharSequence
189 |
190 | /**
191 | * The current query text.
192 | */
193 | private lateinit var mCurrentQuery: CharSequence
194 | //endregion
195 |
196 | //region Listeners
197 | /**
198 | * Listener for when the query text is submitted or changed.
199 | */
200 | private var mOnQueryTextListener: OnQueryTextListener? = null
201 |
202 | /**
203 | * Listener for when the search view opens and closes.
204 | */
205 | private var mSearchViewListener: SearchViewListener? = null
206 |
207 | /**
208 | * Listener for interaction with the voice button.
209 | */
210 | private var mOnVoiceClickedListener: OnVoiceClickedListener? = null
211 |
212 | /**
213 | * Listener for interaction with the clear (X) button
214 | */
215 | private var mOnClearClickListener: OnClearTextClickListener? = null
216 | //endregion
217 |
218 | //region Initializers
219 | /**
220 | * Preforms any required initializations for the search view.
221 | */
222 | private fun init() {
223 | // Inflate view
224 | LayoutInflater.from(mContext).inflate(R.layout.search_view, this, true)
225 |
226 | // Get items
227 | mRoot = findViewById(R.id.search_layout)
228 | mTintView = mRoot.findViewById(R.id.transparent_view)
229 | mSearchBar = mRoot.findViewById(R.id.search_bar)
230 | mBack = mRoot.findViewById(R.id.action_back)
231 | mSearchEditText = mRoot.findViewById(R.id.et_search)
232 | mVoice = mRoot.findViewById(R.id.action_voice)
233 | mClear = mRoot.findViewById(R.id.action_clear)
234 | mSuggestionsListView = mRoot.findViewById(R.id.suggestion_list)
235 |
236 | // Set click listeners
237 | mBack.setOnClickListener { closeSearch() }
238 | mVoice.setOnClickListener { onVoiceClicked() }
239 | mClear.setOnClickListener { onClearClicked() }
240 | mTintView.setOnClickListener {
241 | if (mShouldCloseOnTintClick) {
242 | closeSearch()
243 | }
244 | }
245 |
246 | // Initialize the search view.
247 | initSearchView()
248 | adapter = CursorSearchAdapter(mContext, historyCursor, 0)
249 |
250 | adapter.setFilterQueryProvider(FilterQueryProvider { constraint ->
251 | val filter = constraint.toString()
252 | if (filter.isEmpty()) {
253 | historyCursor
254 | } else {
255 | mContext.contentResolver.query(
256 | HistoryContract.HistoryEntry.CONTENT_URI,
257 | null,
258 | HistoryContract.HistoryEntry.COLUMN_QUERY + " LIKE ?", arrayOf("%$filter%"),
259 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY + " DESC, " +
260 | HistoryContract.HistoryEntry.COLUMN_QUERY
261 | )
262 | }
263 | })
264 | mSuggestionsListView.adapter = adapter
265 | mSuggestionsListView.isTextFilterEnabled = true
266 | mSuggestionsListView.onItemClickListener = OnItemClickListener { _, _, position, _ ->
267 | val suggestion = getSuggestionAtPosition(position)
268 | setQuery(suggestion, true)
269 | }
270 | }
271 |
272 | /**
273 | * Initializes the style of this view.
274 | * @param attributeSet The attributes to apply to the view.
275 | * @param defStyleAttribute An attribute to the style theme applied to this view.
276 | */
277 | private fun initStyle(attributeSet: AttributeSet?, defStyleAttribute: Int) {
278 | AppCompatDelegate.setCompatVectorFromResourcesEnabled(true)
279 | val typedArray = mContext.obtainStyledAttributes(attributeSet, R.styleable.MaterialSearchView, defStyleAttribute, 0)
280 |
281 | if (typedArray.hasValue(R.styleable.MaterialSearchView_searchBackground)) {
282 | background = typedArray.getDrawable(R.styleable.MaterialSearchView_searchBackground)!!
283 | }
284 | if (typedArray.hasValue(R.styleable.MaterialSearchView_android_textColor)) {
285 | setTextColor(typedArray.getColor(R.styleable.MaterialSearchView_android_textColor,
286 | ContextCompat.getColor(mContext, R.color.black)))
287 | }
288 | if (typedArray.hasValue(R.styleable.MaterialSearchView_android_textColorHint)) {
289 | setHintTextColor(typedArray.getColor(R.styleable.MaterialSearchView_android_textColorHint,
290 | ContextCompat.getColor(mContext, R.color.gray_50)))
291 | }
292 | if (typedArray.hasValue(R.styleable.MaterialSearchView_android_hint)) {
293 | setHint(typedArray.getString(R.styleable.MaterialSearchView_android_hint))
294 | }
295 | if (typedArray.hasValue(R.styleable.MaterialSearchView_searchVoiceIcon)) {
296 | setVoiceIcon(typedArray.getResourceId(
297 | R.styleable.MaterialSearchView_searchVoiceIcon,
298 | R.drawable.ic_action_voice_search)
299 | )
300 | }
301 | if (typedArray.hasValue(R.styleable.MaterialSearchView_searchCloseIcon)) {
302 | setClearIcon(typedArray.getResourceId(
303 | R.styleable.MaterialSearchView_searchCloseIcon,
304 | R.drawable.ic_action_navigation_close)
305 | )
306 | }
307 | if (typedArray.hasValue(R.styleable.MaterialSearchView_searchBackIcon)) {
308 | setBackIcon(typedArray.getResourceId(
309 | R.styleable.MaterialSearchView_searchBackIcon,
310 | R.drawable.ic_action_navigation_arrow_back)
311 | )
312 | }
313 | if (typedArray.hasValue(R.styleable.MaterialSearchView_searchSuggestionBackground)) {
314 | setSuggestionBackground(typedArray.getResourceId(
315 | R.styleable.MaterialSearchView_searchSuggestionBackground,
316 | R.color.search_layover_bg)
317 | )
318 | }
319 | if (typedArray.hasValue(R.styleable.MaterialSearchView_historyIcon) && adapter is CursorSearchAdapter) {
320 | (adapter as CursorSearchAdapter).historyIcon = typedArray.getResourceId(
321 | R.styleable.MaterialSearchView_historyIcon,
322 | R.drawable.ic_history_white)
323 | }
324 | if (typedArray.hasValue(R.styleable.MaterialSearchView_suggestionIcon) && adapter is CursorSearchAdapter) {
325 | (adapter as CursorSearchAdapter).suggestionIcon = typedArray.getResourceId(
326 | R.styleable.MaterialSearchView_suggestionIcon,
327 | R.drawable.ic_action_search_white)
328 | }
329 | if (typedArray.hasValue(R.styleable.MaterialSearchView_listTextColor) && adapter is CursorSearchAdapter) {
330 | (adapter as CursorSearchAdapter).textColor = typedArray.getColor(R.styleable.MaterialSearchView_listTextColor,
331 | ContextCompat.getColor(mContext, R.color.white))
332 | }
333 | if (typedArray.hasValue(R.styleable.MaterialSearchView_android_inputType)) {
334 | setInputType(typedArray.getInteger(
335 | R.styleable.MaterialSearchView_android_inputType,
336 | InputType.TYPE_CLASS_TEXT)
337 | )
338 | }
339 | if (typedArray.hasValue(R.styleable.MaterialSearchView_searchBarHeight)) {
340 | setSearchBarHeight(typedArray.getDimensionPixelSize(R.styleable.MaterialSearchView_searchBarHeight, appCompatActionBarHeight))
341 | } else {
342 | setSearchBarHeight(appCompatActionBarHeight)
343 | }
344 | if (typedArray.hasValue(R.styleable.MaterialSearchView_voiceHintPrompt)) {
345 | setVoiceHintPrompt(typedArray.getString(R.styleable.MaterialSearchView_voiceHintPrompt) ?: EMPTY_STRING)
346 | } else {
347 | setVoiceHintPrompt(mContext.getString(R.string.hint_prompt))
348 | }
349 | if (typedArray.hasValue(R.styleable.MaterialSearchView_voiceIconEnabled)) {
350 | isVoiceIconEnabled = typedArray.getBoolean(R.styleable.MaterialSearchView_voiceIconEnabled, true)
351 | }
352 | fitsSystemWindows = false
353 | typedArray.recycle()
354 |
355 | // Show voice button. We put this here because whether or not it's shown is defined by a style above.
356 | displayVoiceButton(true)
357 | }
358 |
359 | /**
360 | * Preforms necessary initializations on the SearchView.
361 | */
362 | private fun initSearchView() {
363 | mSearchEditText.setOnEditorActionListener { v, actionId, event -> // When an edit occurs, submit the query.
364 | onSubmitQuery()
365 | true
366 | }
367 | mSearchEditText.addTextChangedListener(object : TextWatcher {
368 | override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
369 | override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
370 | // When the text changes, filter
371 | adapter.filter.filter(s.toString())
372 | adapter.notifyDataSetChanged()
373 | this@MaterialSearchView.onTextChanged(s)
374 | }
375 |
376 | override fun afterTextChanged(s: Editable) {}
377 | })
378 | mSearchEditText.onFocusChangeListener = OnFocusChangeListener { v, hasFocus -> // If we gain focus, show keyboard and show suggestions.
379 | if (hasFocus) {
380 | showKeyboard(mSearchEditText)
381 | showSuggestions()
382 | }
383 | }
384 | }
385 | //endregion
386 | //region Show Methods
387 | /**
388 | * Displays the keyboard with a focus on the Search EditText.
389 | * @param view The view to attach the keyboard to.
390 | */
391 | private fun showKeyboard(view: View?) {
392 | view?.requestFocus()
393 | if (isHardKeyboardAvailable.not()) {
394 | val inputMethodManager = view?.context?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
395 | inputMethodManager?.showSoftInput(view, 0)
396 | }
397 | }
398 |
399 | /**
400 | * Method that checks if there's a physical keyboard on the phone.
401 | *
402 | * @return true if there's a physical keyboard connected, false otherwise.
403 | */
404 | private val isHardKeyboardAvailable: Boolean
405 | get() = mContext.resources.configuration.keyboard != Configuration.KEYBOARD_NOKEYS
406 |
407 | /**
408 | * Changes the visibility of the voice button to VISIBLE or GONE.
409 | * @param display True to display the voice button, false to hide it.
410 | */
411 | private fun displayVoiceButton(display: Boolean) {
412 | // Only display voice if we pass in true, and it's available
413 | if (display && isVoiceAvailable && isVoiceIconEnabled) {
414 | mVoice.visibility = VISIBLE
415 | } else {
416 | mVoice.visibility = GONE
417 | }
418 | }
419 |
420 | /**
421 | * Changes the visibility of the clear button to VISIBLE or GONE.
422 | * @param display True to display the clear button, false to hide it.
423 | */
424 | private fun displayClearButton(display: Boolean) {
425 | mClear.visibility = if (display) VISIBLE else GONE
426 | }
427 |
428 | /**
429 | * Displays the available suggestions, if any.
430 | */
431 | private fun showSuggestions() {
432 | mSuggestionsListView.visibility = VISIBLE
433 | }
434 |
435 | /**
436 | * Displays the SearchView.
437 | */
438 | fun openSearch() {
439 | // If search is already open, just return.
440 | if (isOpen) {
441 | return
442 | }
443 |
444 | // Get focus
445 | mSearchEditText.setText(EMPTY_STRING)
446 | mSearchEditText.requestFocus()
447 | if (mShouldAnimate) {
448 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
449 | mRoot.visibility = VISIBLE
450 | circleRevealView(mSearchBar)
451 | } else {
452 | fadeInView(mRoot)
453 | }
454 | } else {
455 | mRoot.visibility = VISIBLE
456 | }
457 |
458 | mSearchViewListener?.onSearchViewOpened()
459 |
460 | isOpen = true
461 | }
462 | //endregion
463 | //region Hide Methods
464 | /**
465 | * Hides the suggestion list.
466 | */
467 | private fun dismissSuggestions() {
468 | mSuggestionsListView.visibility = GONE
469 | }
470 |
471 | /**
472 | * Hides the keyboard displayed for the SearchEditText.
473 | * @param view The view to detach the keyboard from.
474 | */
475 | private fun hideKeyboard(view: View) {
476 | val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
477 | inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
478 | }
479 |
480 | /**
481 | * Closes the search view if necessary.
482 | */
483 | fun closeSearch() {
484 | // If we're already closed, just return.
485 | if (!isOpen) {
486 | return
487 | }
488 |
489 | // Clear text, values, and focus.
490 | mSearchEditText.setText(EMPTY_STRING)
491 | dismissSuggestions()
492 | clearFocus()
493 | if (mShouldAnimate) {
494 | val v: View = mRoot
495 | val listenerAdapter: AnimatorListenerAdapter = object : AnimatorListenerAdapter() {
496 | override fun onAnimationEnd(animation: Animator) {
497 | super.onAnimationEnd(animation)
498 | // After the animation is done. Hide the root view.
499 | v.visibility = GONE
500 | }
501 | }
502 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
503 | circleHideView(mSearchBar, listenerAdapter)
504 | } else {
505 | fadeOutView(mRoot)
506 | }
507 | } else {
508 | // Just hide the view.
509 | mRoot.visibility = GONE
510 | }
511 |
512 |
513 | // Call listener if we have one
514 | mSearchViewListener?.onSearchViewClosed()
515 |
516 | isOpen = false
517 | }
518 | //endregion
519 | //region Interface Methods
520 | /**
521 | * Filters and updates the buttons when text is changed.
522 | * @param newText The new text.
523 | */
524 | private fun onTextChanged(newText: CharSequence) {
525 | // Get current query
526 | mCurrentQuery = mSearchEditText.text
527 |
528 | // If the text is not empty, show the empty button and hide the voice button
529 | if (!TextUtils.isEmpty(mCurrentQuery)) {
530 | displayVoiceButton(false)
531 | displayClearButton(true)
532 | } else {
533 | displayClearButton(false)
534 | displayVoiceButton(true)
535 | }
536 |
537 | // If we have a query listener and the text has changed, call it.
538 | mOnQueryTextListener?.onQueryTextChange(newText.toString())
539 |
540 | mOldQuery = mCurrentQuery
541 | }
542 |
543 | /**
544 | * Called when a query is submitted. This will close the search view.
545 | */
546 | private fun onSubmitQuery() {
547 | // Get the query.
548 | val query: CharSequence? = mSearchEditText.text
549 |
550 | // If the query is not null and it has some text, submit it.
551 | if (query != null && TextUtils.getTrimmedLength(query) > 0) {
552 |
553 | // If we don't have a listener, or if the search view handled the query, close it.
554 | // TODO - Improve.
555 | if (mOnQueryTextListener?.onQueryTextSubmit(query.toString()) == false) {
556 | if (mShouldKeepHistory) {
557 | saveQueryToDb(query.toString(), System.currentTimeMillis())
558 | }
559 |
560 | // Refresh the cursor on the adapter,
561 | // so the new entry will be shown on the next time the user opens the search view.
562 | refreshAdapterCursor()
563 | closeSearch()
564 | mSearchEditText.setText(EMPTY_STRING)
565 | }
566 | }
567 | }
568 |
569 | /**
570 | * Handles when the voice button is clicked and starts listening, then calls activity with voice search.
571 | */
572 | private fun onVoiceClicked() {
573 | // If the user has their own OnVoiceClickedListener defined, call that. Otherwise, use
574 | // the library default.
575 | if (mOnVoiceClickedListener != null) {
576 | mOnVoiceClickedListener?.onVoiceClicked()
577 | } else {
578 | val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
579 | intent.putExtra(RecognizerIntent.EXTRA_PROMPT, mHintPrompt)
580 | intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
581 | intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, MAX_RESULTS) // Quantity of results we want to receive
582 | if (mContext is Activity) {
583 | mContext.startActivityForResult(intent, REQUEST_VOICE)
584 | }
585 | }
586 | }
587 |
588 | /**
589 | * Handles when the clear (X) button is clicked.
590 | */
591 | private fun onClearClicked() {
592 | mOnClearClickListener?.onClearClicked()
593 | mSearchEditText.setText(EMPTY_STRING)
594 | }
595 |
596 | //endregion
597 |
598 | //region Mutators
599 | fun setOnQueryTextListener(mOnQueryTextListener: OnQueryTextListener?) {
600 | this.mOnQueryTextListener = mOnQueryTextListener
601 | }
602 |
603 | fun setSearchViewListener(mSearchViewListener: SearchViewListener?) {
604 | this.mSearchViewListener = mSearchViewListener
605 | }
606 |
607 | /**
608 | * Sets an OnItemClickListener to the suggestion list.
609 | *
610 | * @param listener - The ItemClickListener.
611 | */
612 | fun setOnItemClickListener(listener: OnItemClickListener?) {
613 | mSuggestionsListView.onItemClickListener = listener
614 | }
615 |
616 | /**
617 | * Sets an OnItemLongClickListener to the suggestion list.
618 | *
619 | * @param listener - The ItemLongClickListener.
620 | */
621 | fun setOnItemLongClickListener(listener: OnItemLongClickListener?) {
622 | mSuggestionsListView.onItemLongClickListener = listener
623 | }
624 |
625 | /**
626 | * Toggles the Tint click action.
627 | *
628 | * @param shouldClose - Whether the tint click should close the search view or not.
629 | */
630 | fun setCloseOnTintClick(shouldClose: Boolean) {
631 | mShouldCloseOnTintClick = shouldClose
632 | }
633 |
634 | /**
635 | * Sets whether the MSV should be animated on open/close or not.
636 | *
637 | * @param mShouldAnimate - true if you want animations, false otherwise.
638 | */
639 | fun setShouldAnimate(mShouldAnimate: Boolean) {
640 | this.mShouldAnimate = mShouldAnimate
641 | }
642 |
643 | /**
644 | * Sets whether the MSV should be keeping track of the submited queries or not.
645 | *
646 | * @param keepHistory - true if you want to save the search history, false otherwise.
647 | */
648 | fun setShouldKeepHistory(keepHistory: Boolean) {
649 | mShouldKeepHistory = keepHistory
650 | }
651 |
652 | /**
653 | * Set the query to search view. If submit is set to true, it'll submit the query.
654 | *
655 | * @param query - The Query value.
656 | * @param submit - Whether to submit or not the query or not.
657 | */
658 | fun setQuery(query: CharSequence?, submit: Boolean) {
659 | mSearchEditText.setText(query)
660 | if (query != null) {
661 | mSearchEditText.setSelection(mSearchEditText.length())
662 | mCurrentQuery = query
663 | }
664 | if (submit && !TextUtils.isEmpty(query)) {
665 | onSubmitQuery()
666 | }
667 | }
668 |
669 | /**
670 | * Sets the background of the SearchView.
671 | * @param background The drawable to use as a background.
672 | */
673 | override fun setBackground(background: Drawable) {
674 | // Method changed in jelly bean for setting background.
675 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
676 | mTintView.background = background
677 | } else {
678 | mTintView.setBackgroundDrawable(background)
679 | }
680 | }
681 |
682 | /**
683 | * Sets the background color of the SearchView.
684 | *
685 | * @param color The color to use for the background.
686 | */
687 | override fun setBackgroundColor(color: Int) {
688 | setTintColor(color)
689 | }
690 |
691 | fun setSearchBarColor(color: Int) {
692 | // Set background color of search bar.
693 | mSearchEditText.setBackgroundColor(color)
694 | }
695 |
696 | /**
697 | * Change the color of the background tint.
698 | *
699 | * @param color The new color.
700 | */
701 | private fun setTintColor(color: Int) {
702 | mTintView.setBackgroundColor(color)
703 | }
704 |
705 | /**
706 | * Sets the alpha value of the background tint.
707 | * @param alpha The alpha value, from 0 to 255.
708 | */
709 | fun setTintAlpha(alpha: Int) {
710 | if (alpha < 0 || alpha > 255) return
711 | val d = mTintView.background
712 | if (d is ColorDrawable) {
713 | val color = d.color
714 | val newColor = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
715 | setTintColor(newColor)
716 | }
717 | }
718 |
719 | /**
720 | * Adjust the background tint alpha, based on a percentage.
721 | *
722 | * @param factor The factor of the alpha, from 0% to 100%.
723 | */
724 | fun adjustTintAlpha(factor: Float) {
725 | if (factor < 0 || factor > 1.0) return
726 | val d = mTintView.background
727 | if (d is ColorDrawable) {
728 | var color = d.color
729 | color = adjustAlpha(color, factor)
730 | mTintView.setBackgroundColor(color)
731 | }
732 | }
733 |
734 | /**
735 | * Adjust the alpha of a color based on a percent factor.
736 | *
737 | * @param color - The color you want to change the alpha value.
738 | * @param factor - The factor of the alpha, from 0% to 100%.
739 | * @return The color with the adjusted alpha value.
740 | */
741 | private fun adjustAlpha(color: Int, factor: Float): Int {
742 | if (factor < 0) return color
743 | val alpha = (Color.alpha(color) * factor).roundToInt()
744 | return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color))
745 | }
746 |
747 | /**
748 | * Sets the text color of the EditText.
749 | * @param color The color to use for the EditText.
750 | */
751 | fun setTextColor(color: Int) {
752 | mSearchEditText.setTextColor(color)
753 | }
754 |
755 | /**
756 | * Sets the text color of the search hint.
757 | * @param color The color to be used for the hint text.
758 | */
759 | fun setHintTextColor(color: Int) {
760 | mSearchEditText.setHintTextColor(color)
761 | }
762 |
763 | /**
764 | * Sets the hint to be used for the search EditText.
765 | * @param hint The hint to be displayed in the search EditText.
766 | */
767 | fun setHint(hint: CharSequence?) {
768 | mSearchEditText.hint = hint
769 | }
770 |
771 | /**
772 | * Sets the icon for the voice action.
773 | * @param resourceId The drawable to represent the voice action.
774 | */
775 | fun setVoiceIcon(resourceId: Int) {
776 | mVoice.setImageResource(resourceId)
777 | }
778 |
779 | /**
780 | * Sets the icon for the clear action.
781 | * @param resourceId The resource ID of drawable that will represent the clear action.
782 | */
783 | fun setClearIcon(resourceId: Int) {
784 | mClear.setImageResource(resourceId)
785 | }
786 |
787 | /**
788 | * Sets the icon for the back action.
789 | * @param resourceId The resource Id of the drawable that will represent the back action.
790 | */
791 | fun setBackIcon(resourceId: Int) {
792 | mBack.setImageResource(resourceId)
793 | }
794 |
795 | /**
796 | * Sets the background of the suggestions ListView.
797 | *
798 | * @param resource The resource to use as a background for the
799 | * suggestions listview.
800 | */
801 | fun setSuggestionBackground(resource: Int) {
802 | if (resource > 0) {
803 | mSuggestionsListView.setBackgroundResource(resource)
804 | }
805 | }
806 |
807 | /**
808 | * Changes the default history list icon.
809 | *
810 | * @param resourceId The resource id of the new history icon.
811 | */
812 | fun setHistoryIcon(@DrawableRes resourceId: Int) {
813 | adapter.let {
814 | if (it is CursorSearchAdapter) {
815 | it.historyIcon = resourceId
816 | }
817 | }
818 | }
819 |
820 | /**
821 | * Changes the default suggestion list icon.
822 | *
823 | * @param resourceId The resource id of the new suggestion icon.
824 | */
825 | fun setSuggestionIcon(@DrawableRes resourceId: Int) {
826 | adapter.let {
827 | if (it is CursorSearchAdapter) {
828 | it.suggestionIcon = resourceId
829 | }
830 | }
831 | }
832 |
833 | /**
834 | * Changes the default suggestion list item text color.
835 | *
836 | * @param color The new color.
837 | */
838 | fun setListTextColor(color: Int) {
839 | adapter.let {
840 | if (it is CursorSearchAdapter) {
841 | it.textColor = color
842 | }
843 | }
844 | }
845 |
846 | /**
847 | * Sets the input type of the SearchEditText.
848 | *
849 | * @param inputType The input type to set to the EditText.
850 | */
851 | fun setInputType(inputType: Int) {
852 | mSearchEditText.inputType = inputType
853 | }
854 |
855 | /**
856 | * Sets a click listener for the voice button.
857 | */
858 | fun setOnVoiceClickedListener(listener: OnVoiceClickedListener?) {
859 | mOnVoiceClickedListener = listener
860 | }
861 |
862 | fun setOnVoiceClickedListener(listener: () -> Unit) {
863 | setOnVoiceClickedListener(object: OnVoiceClickedListener {
864 | override fun onVoiceClicked() {
865 | listener.invoke()
866 | }
867 | })
868 | }
869 |
870 | /**
871 | * Sets a click listener for the clear (X) button.
872 | */
873 | fun setOnClearClickListener(listener: OnClearTextClickListener) {
874 | mOnClearClickListener = listener
875 | }
876 |
877 | fun setOnClearClickListener(listener: () -> Unit) {
878 | setOnClearClickListener(object: OnClearTextClickListener {
879 | override fun onClearClicked() {
880 | listener.invoke()
881 | }
882 | })
883 | }
884 |
885 | /**
886 | * Sets the bar height if prefered to not use the existing actionbar height value
887 | *
888 | * @param height The value of the height in pixels
889 | */
890 | fun setSearchBarHeight(height: Int) {
891 | mSearchBar.minimumHeight = height
892 | mSearchBar.layoutParams.height = height
893 | }
894 |
895 | fun setVoiceHintPrompt(hintPrompt: String) {
896 | mHintPrompt = if (!TextUtils.isEmpty(hintPrompt)) {
897 | hintPrompt
898 | } else {
899 | mContext.getString(R.string.hint_prompt)
900 | }
901 | }
902 |
903 | /**
904 | * Returns the actual AppCompat ActionBar height value. This will be used as the default
905 | *
906 | * @return The value of the actual actionbar height in pixels
907 | */
908 | private val appCompatActionBarHeight: Int
909 | get() {
910 | val tv = TypedValue()
911 | context.theme.resolveAttribute(R.attr.actionBarSize, tv, true)
912 | return resources.getDimensionPixelSize(tv.resourceId)
913 | }
914 | //endregion
915 |
916 | //region Accessors
917 | /**
918 | * Gets the current text on the SearchView, if any. Returns an empty String if no text is available.
919 | * @return The current query, or an empty String if there's no query.
920 | */
921 | val currentQuery: String
922 | get() = if (!TextUtils.isEmpty(mCurrentQuery)) {
923 | mCurrentQuery.toString()
924 | } else EMPTY_STRING// Get package manager
925 |
926 | // Gets a list of activities that can handle this intent.
927 |
928 | // Returns true if we have at least one activity.
929 | /** Determines if the user's voice is available
930 | * @return True if we can collect the user's voice, false otherwise.
931 | */
932 | private val isVoiceAvailable: Boolean
933 | get() {
934 | // Get package manager
935 | val packageManager = mContext.packageManager
936 |
937 | // Gets a list of activities that can handle this intent.
938 | val activities = packageManager.queryIntentActivities(Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0)
939 |
940 | // Returns true if we have at least one activity.
941 | return activities.size > 0
942 | }
943 |
944 | /**
945 | * Retrieves a suggestion at a given index in the adapter.
946 | *
947 | * @return The search suggestion for that index.
948 | */
949 | fun getSuggestionAtPosition(position: Int): String {
950 | // If position is out of range just return empty string.
951 | return if (position < 0 || position >= adapter.count) {
952 | EMPTY_STRING
953 | } else {
954 | adapter.getItem(position).toString()
955 | }
956 | }
957 | //endregion
958 |
959 | //region View Methods
960 | /**
961 | * Handles any cleanup when focus is cleared from the view.
962 | */
963 | override fun clearFocus() {
964 | mClearingFocus = true
965 | hideKeyboard(this)
966 | super.clearFocus()
967 | mSearchEditText.clearFocus()
968 | mClearingFocus = false
969 | }
970 |
971 | override fun requestFocus(direction: Int, previouslyFocusedRect: Rect?): Boolean {
972 | // Don't accept if we are clearing focus, or if the view isn't focusable.
973 | return !(mClearingFocus || !isFocusable) && mSearchEditText.requestFocus(direction, previouslyFocusedRect)
974 | }
975 |
976 | //----- Lifecycle methods -----//
977 | // public void activityPaused() {
978 | // Cursor cursor = ((CursorAdapter)mAdapter).getCursor();
979 | // if (cursor != null && !cursor.isClosed()) {
980 | // cursor.close();
981 | // }
982 | // }
983 | fun activityResumed() {
984 | refreshAdapterCursor()
985 | }
986 | //endregion
987 |
988 | //region Database Methods
989 | /**
990 | * Save a query to the local database.
991 | *
992 | * @param query - The query to be saved. Can't be empty or null.
993 | * @param ms - The insert date, in millis. As a suggestion, use System.currentTimeMillis();
994 | */
995 | @Synchronized
996 | fun saveQueryToDb(query: String?, ms: Long) {
997 | if (!TextUtils.isEmpty(query) && ms > 0) {
998 | val values = ContentValues()
999 | values.put(HistoryContract.HistoryEntry.COLUMN_QUERY, query)
1000 | values.put(HistoryContract.HistoryEntry.COLUMN_INSERT_DATE, ms)
1001 | values.put(HistoryContract.HistoryEntry.COLUMN_IS_HISTORY, 1) // Saving as history.
1002 | mContext.contentResolver.insert(HistoryContract.HistoryEntry.CONTENT_URI, values)
1003 | }
1004 | }
1005 |
1006 | /**
1007 | * Add a single suggestion item to the suggestion list.
1008 | * @param suggestion - The suggestion to be inserted on the database.
1009 | */
1010 | @Synchronized
1011 | fun addSuggestion(suggestion: String?) {
1012 | if (suggestion?.isNotEmpty() == true) {
1013 | val value = ContentValues()
1014 | value.put(HistoryContract.HistoryEntry.COLUMN_QUERY, suggestion)
1015 | value.put(HistoryContract.HistoryEntry.COLUMN_INSERT_DATE, System.currentTimeMillis())
1016 | value.put(HistoryContract.HistoryEntry.COLUMN_IS_HISTORY, 0) // Saving as suggestion.
1017 | mContext.contentResolver.insert(
1018 | HistoryContract.HistoryEntry.CONTENT_URI,
1019 | value
1020 | )
1021 | }
1022 | }
1023 |
1024 | /**
1025 | * Removes a single suggestion from the list.
1026 | * Disclaimer, this doesn't remove a single search history item, only suggestions.
1027 | * @param suggestion - The suggestion to be removed.
1028 | */
1029 | @Synchronized
1030 | fun removeSuggestion(suggestion: String?) {
1031 | if (suggestion?.isNotEmpty() == true) {
1032 | mContext.contentResolver.delete(
1033 | HistoryContract.HistoryEntry.CONTENT_URI,
1034 | HistoryContract.HistoryEntry.TABLE_NAME +
1035 | "." +
1036 | HistoryContract.HistoryEntry.COLUMN_QUERY +
1037 | " = ? AND " +
1038 | HistoryContract.HistoryEntry.TABLE_NAME +
1039 | "." +
1040 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY +
1041 | " = ?", arrayOf(suggestion, 0.toString()))
1042 | }
1043 | }
1044 |
1045 | @Synchronized
1046 | fun addSuggestions(suggestions: List) {
1047 | val toSave = ArrayList()
1048 | for (str in suggestions) {
1049 | val value = ContentValues()
1050 | value.put(HistoryContract.HistoryEntry.COLUMN_QUERY, str)
1051 | value.put(HistoryContract.HistoryEntry.COLUMN_INSERT_DATE, System.currentTimeMillis())
1052 | value.put(HistoryContract.HistoryEntry.COLUMN_IS_HISTORY, 0) // Saving as suggestion.
1053 | toSave.add(value)
1054 | }
1055 | val values = toSave.toTypedArray()
1056 | mContext.contentResolver.bulkInsert(
1057 | HistoryContract.HistoryEntry.CONTENT_URI,
1058 | values
1059 | )
1060 | }
1061 |
1062 | fun addSuggestions(suggestions: Array) {
1063 | val list = ArrayList(listOf(*suggestions))
1064 | addSuggestions(list)
1065 | }
1066 |
1067 | private val historyCursor: Cursor?
1068 | get() = mContext.contentResolver.query(
1069 | HistoryContract.HistoryEntry.CONTENT_URI,
1070 | null,
1071 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY + " = ?", arrayOf("1"),
1072 | HistoryContract.HistoryEntry.COLUMN_INSERT_DATE + " DESC LIMIT " + MAX_HISTORY
1073 | )
1074 |
1075 | private fun refreshAdapterCursor() {
1076 | val historyCursor = historyCursor
1077 | adapter.changeCursor(historyCursor)
1078 | }
1079 |
1080 | @Synchronized
1081 | fun clearSuggestions() {
1082 | mContext.contentResolver.delete(
1083 | HistoryContract.HistoryEntry.CONTENT_URI,
1084 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY + " = ?", arrayOf("0"))
1085 | }
1086 |
1087 | @Synchronized
1088 | fun clearHistory() {
1089 | mContext.contentResolver.delete(
1090 | HistoryContract.HistoryEntry.CONTENT_URI,
1091 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY + " = ?", arrayOf("1"))
1092 | }
1093 |
1094 | @Synchronized
1095 | fun clearAll() {
1096 | mContext.contentResolver.delete(
1097 | HistoryContract.HistoryEntry.CONTENT_URI,
1098 | null,
1099 | null
1100 | )
1101 | }
1102 | //endregion
1103 | //region Interfaces
1104 | /**
1105 | * Interface that handles the submission and change of search queries.
1106 | */
1107 | interface OnQueryTextListener {
1108 | /**
1109 | * Called when a search query is submitted.
1110 | *
1111 | * @param query The text that will be searched.
1112 | * @return True when the query is handled by the listener, false to let the SearchView handle the default case.
1113 | */
1114 | fun onQueryTextSubmit(query: String): Boolean
1115 |
1116 | /**
1117 | * Called when a search query is changed.
1118 | *
1119 | * @param newText The new text of the search query.
1120 | * @return True when the query is handled by the listener, false to let the SearchView handle the default case.
1121 | */
1122 | fun onQueryTextChange(newText: String): Boolean
1123 | }
1124 |
1125 | /**
1126 | * Interface that handles the opening and closing of the SearchView.
1127 | */
1128 | interface SearchViewListener {
1129 | /**
1130 | * Called when the searchview is opened.
1131 | */
1132 | fun onSearchViewOpened()
1133 |
1134 | /**
1135 | * Called when the search view closes.
1136 | */
1137 | fun onSearchViewClosed()
1138 | }
1139 |
1140 | /**
1141 | * Interface that handles interaction with the voice button.
1142 | */
1143 | interface OnVoiceClickedListener {
1144 | /**
1145 | * Called when the user clicks the voice button.
1146 | */
1147 | fun onVoiceClicked()
1148 | }
1149 |
1150 | /**
1151 | * Handles interactions with the clear (X) button
1152 | */
1153 | interface OnClearTextClickListener {
1154 | fun onClearClicked()
1155 | }
1156 | //endregion
1157 | }
1158 |
--------------------------------------------------------------------------------
/library/src/main/java/br/com/mauker/materialsearchview/adapters/CursorSearchAdapter.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview.adapters
2 |
3 | import android.content.Context
4 | import android.database.Cursor
5 | import android.graphics.Color
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.widget.CursorAdapter
10 | import android.widget.ImageView
11 | import android.widget.TextView
12 |
13 | import br.com.mauker.materialsearchview.R
14 | import br.com.mauker.materialsearchview.db.HistoryContract
15 |
16 | /**
17 | * Created by mauker on 19/04/2016.
18 | *
19 | * Default adapter used for the suggestion/history ListView.
20 | */
21 | open class CursorSearchAdapter @JvmOverloads constructor(
22 | context: Context,
23 | cursor: Cursor?,
24 | flags: Int = 0
25 | ) : CursorAdapter(context, cursor, flags) {
26 |
27 | var textColor = Color.WHITE
28 | var historyIcon = R.drawable.ic_history_white
29 | var suggestionIcon = R.drawable.ic_action_search_white
30 |
31 | override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View {
32 | return LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
33 | }
34 |
35 | override fun bindView(view: View, context: Context, cursor: Cursor) {
36 | val vh = ListViewHolder(view)
37 | view.tag = vh
38 |
39 | val text = cursor.getString(cursor.getColumnIndexOrThrow(HistoryContract.HistoryEntry.COLUMN_QUERY))
40 |
41 | val isHistory = cursor.getInt(cursor.getColumnIndexOrThrow(
42 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY)) != 0
43 |
44 | val historyItem = SearchHistoryItem(text, isHistory)
45 | vh.bindItem(historyItem)
46 | }
47 |
48 | override fun getItem(position: Int): Any {
49 | var retString = ""
50 |
51 | // Move to position, get query
52 | val cursor = cursor
53 | if (cursor.moveToPosition(position)) {
54 | retString = cursor.getString(cursor.getColumnIndexOrThrow(HistoryContract.HistoryEntry.COLUMN_QUERY))
55 | }
56 |
57 | return retString
58 | }
59 |
60 | private inner class ListViewHolder (convertView: View) {
61 | private val iv_icon: ImageView = convertView.findViewById(R.id.iv_icon)
62 | private val tv_content: TextView = convertView.findViewById(R.id.tv_str)
63 |
64 | fun bindItem(item: SearchHistoryItem) {
65 | tv_content.text = item.text
66 | tv_content.setTextColor(textColor)
67 |
68 | val iconRes = if (item.isHistory) historyIcon else suggestionIcon
69 | iv_icon.setImageResource(iconRes)
70 | }
71 | }
72 | }
73 |
74 | data class SearchHistoryItem(val text: String, val isHistory: Boolean)
75 |
--------------------------------------------------------------------------------
/library/src/main/java/br/com/mauker/materialsearchview/db/HistoryContract.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview.db
2 |
3 | import android.content.ContentUris
4 | import android.net.Uri
5 | import android.provider.BaseColumns
6 |
7 | /**
8 | * Created by mauker on 15/04/16.
9 | * Database contract. Contains the definition of my tables.
10 | */
11 | object HistoryContract {
12 | @JvmField
13 | val CONTENT_AUTHORITY = initAuthority()
14 | private val BASE_CONTENT_URI = Uri.parse("content://$CONTENT_AUTHORITY")
15 | const val PATH_HISTORY = "history"
16 |
17 | // ----- Authority setup ----- //
18 | private fun initAuthority(): String {
19 | var authority = "br.com.mauker.materialsearchview.defaultsearchhistorydatabase"
20 | try {
21 | val clazzLoader = HistoryContract::class.java.classLoader
22 | val clazz = clazzLoader.loadClass("br.com.mauker.MsvAuthority")
23 | val field = clazz.getDeclaredField("CONTENT_AUTHORITY")
24 | authority = field[null].toString()
25 | } catch (e: ClassNotFoundException) {
26 | e.printStackTrace()
27 | } catch (e: NoSuchFieldException) {
28 | e.printStackTrace()
29 | } catch (e: IllegalAccessException) {
30 | e.printStackTrace()
31 | }
32 | return authority
33 | }
34 |
35 | // ----- Table definitions ----- //
36 | object HistoryEntry {
37 | // Content provider stuff.
38 | @JvmField
39 | val _COUNT: String? = "_count"
40 | @JvmField
41 | val _ID = "_id"
42 | @JvmField
43 | val CONTENT_URI = BASE_CONTENT_URI.buildUpon().appendPath(PATH_HISTORY).build()
44 | @JvmField
45 | val CONTENT_TYPE = "vnd.android.cursor.dir/$CONTENT_URI/$PATH_HISTORY"
46 | @JvmField
47 | val CONTENT_ITEM = "vnd.android.cursor.item/$CONTENT_URI/$PATH_HISTORY"
48 |
49 | // Table definition stuff.
50 | const val TABLE_NAME = "SEARCH_HISTORY"
51 | const val COLUMN_QUERY = "query"
52 | const val COLUMN_INSERT_DATE = "insert_date"
53 | const val COLUMN_IS_HISTORY = "is_history"
54 | @JvmStatic
55 | fun buildHistoryUri(id: Long): Uri {
56 | return ContentUris.withAppendedId(CONTENT_URI, id)
57 | }
58 | } // ----- !Table definitions ----- //
59 | }
--------------------------------------------------------------------------------
/library/src/main/java/br/com/mauker/materialsearchview/db/HistoryDbHelper.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview.db
2 |
3 | import android.content.Context
4 | import android.database.sqlite.SQLiteDatabase
5 | import android.database.sqlite.SQLiteOpenHelper
6 |
7 | /**
8 | * Created by mauker on 15/04/16.
9 | * Helper class used to create and update the database.
10 | */
11 | class HistoryDbHelper
12 | /**
13 | * The default constructor for this helper.
14 | * @param context The Android application context which is using this DB.
15 | */
16 | (context: Context?) : SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
17 | /**
18 | * Called when this database is first created.
19 | * @param db The database being created.
20 | */
21 | override fun onCreate(db: SQLiteDatabase) {
22 | addHistoryTable(db)
23 | }
24 |
25 | /**
26 | * Called whenever DATABASE_VERSION is incremented. This is used whenever schema changes need
27 | * to be made or new tables are added.
28 | *
29 | * @param db The database being updated.
30 | * @param oldVersion The previous version of the database. Used to determine whether or not
31 | * certain updates should be run.
32 | * @param newVersion The new version of the database.
33 | */
34 | override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
35 | // TODO - Create update if needed.
36 | dropAllTables(db)
37 | onCreate(db)
38 | }
39 |
40 | /**
41 | * Called whenever DATABASE_VERSION is decremented.
42 | *
43 | * @param db The database being updated.
44 | * @param oldVersion The previous version of the database. Used to determine whether or not
45 | * certain updates should be run.
46 | * @param newVersion The new version of the database.
47 | */
48 | override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
49 | // TODO - Create downgrade if needed.
50 | dropAllTables(db)
51 | onCreate(db)
52 | }
53 |
54 | /**
55 | * Inserts the Search History table into the database.
56 | * @param db The SQLiteDatabase the table is inserted into.
57 | */
58 | private fun addHistoryTable(db: SQLiteDatabase) {
59 | db.execSQL(
60 | "CREATE TABLE " + HistoryContract.HistoryEntry.TABLE_NAME + " (" +
61 | HistoryContract.HistoryEntry._ID + " INTEGER PRIMARY KEY," +
62 | HistoryContract.HistoryEntry.COLUMN_QUERY + " TEXT NOT NULL," +
63 | HistoryContract.HistoryEntry.COLUMN_INSERT_DATE + " INTEGER DEFAULT 0," +
64 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY + " INTEGER NOT NULL DEFAULT 0," +
65 | "UNIQUE (" + HistoryContract.HistoryEntry.COLUMN_QUERY + ") ON CONFLICT REPLACE);"
66 | )
67 | }
68 |
69 | /**
70 | * Convenience method to drop all tables. Take extreme care with this.
71 | *
72 | * @param db The SQLiteDatabase from where you're dropping the tables.
73 | */
74 | private fun dropAllTables(db: SQLiteDatabase) {
75 | db.execSQL("DROP TABLE IF EXISTS " + HistoryContract.HistoryEntry.TABLE_NAME)
76 | }
77 |
78 | companion object {
79 | // Defines the database version. Increment it if the schema has been changed.
80 | private const val DB_VERSION = 5
81 |
82 | // The name of the database.
83 | private const val DB_NAME = "SearchHistory.db"
84 | }
85 | }
--------------------------------------------------------------------------------
/library/src/main/java/br/com/mauker/materialsearchview/db/HistoryProvider.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview.db
2 |
3 | import android.content.ContentProvider
4 | import android.content.ContentUris
5 | import android.content.ContentValues
6 | import android.content.UriMatcher
7 | import android.database.Cursor
8 | import android.net.Uri
9 | import br.com.mauker.materialsearchview.db.HistoryContract.HistoryEntry.buildHistoryUri
10 |
11 | /**
12 | *
13 | * Created by mauker on 15/04/16.
14 | *
15 | * The content provider for the search history.
16 | *
17 | */
18 | class HistoryProvider : ContentProvider() {
19 |
20 | companion object {
21 | private const val SEARCH_HISTORY = 100
22 | private const val SEARCH_HISTORY_DATE = 101
23 | private const val SEARCH_HISTORY_ID = 102
24 | private const val SEARCH_HISTORY_IS_HISTORY = 103
25 | private val sUriMatcher = buildUriMatcher()
26 |
27 | private fun buildUriMatcher(): UriMatcher {
28 | val content = HistoryContract.CONTENT_AUTHORITY
29 |
30 | // All paths to the UriMatcher have a corresponding code to return
31 | // when a match is found (the ints above).
32 | val matcher = UriMatcher(UriMatcher.NO_MATCH)
33 | matcher.addURI(content, HistoryContract.PATH_HISTORY, SEARCH_HISTORY)
34 | matcher.addURI(content, HistoryContract.PATH_HISTORY + "/#", SEARCH_HISTORY_DATE)
35 | matcher.addURI(content, HistoryContract.PATH_HISTORY + "/#", SEARCH_HISTORY_ID)
36 | matcher.addURI(content, HistoryContract.PATH_HISTORY + "/#", SEARCH_HISTORY_IS_HISTORY)
37 | return matcher
38 | }
39 | }
40 |
41 | private lateinit var mOpenHelper: HistoryDbHelper
42 |
43 | override fun onCreate(): Boolean {
44 | mOpenHelper = HistoryDbHelper(context)
45 | return true
46 | }
47 |
48 | override fun query(uri: Uri, projection: Array?, selection: String?,
49 | selectionArgs: Array?, sortOrder: String?): Cursor? {
50 | val db = mOpenHelper.readableDatabase
51 | val rCursor: Cursor
52 | rCursor = when (sUriMatcher.match(uri)) {
53 | SEARCH_HISTORY -> db.query(
54 | HistoryContract.HistoryEntry.TABLE_NAME,
55 | projection,
56 | selection,
57 | selectionArgs,
58 | null,
59 | null,
60 | sortOrder
61 | )
62 | SEARCH_HISTORY_DATE -> {
63 | val date = ContentUris.parseId(uri)
64 | db.query(
65 | HistoryContract.HistoryEntry.TABLE_NAME,
66 | projection,
67 | HistoryContract.HistoryEntry.COLUMN_INSERT_DATE + " = ?", arrayOf(date.toString()),
68 | null,
69 | null,
70 | sortOrder
71 | )
72 | }
73 | SEARCH_HISTORY_ID -> {
74 | val id = ContentUris.parseId(uri)
75 | db.query(
76 | HistoryContract.HistoryEntry.TABLE_NAME,
77 | projection,
78 | HistoryContract.HistoryEntry._ID + " = ?", arrayOf(id.toString()),
79 | null,
80 | null,
81 | sortOrder
82 | )
83 | }
84 | SEARCH_HISTORY_IS_HISTORY -> {
85 | val flag = ContentUris.parseId(uri)
86 | db.query(
87 | HistoryContract.HistoryEntry.TABLE_NAME,
88 | projection,
89 | HistoryContract.HistoryEntry.COLUMN_IS_HISTORY + " = ?", arrayOf(flag.toString()),
90 | null,
91 | null,
92 | sortOrder
93 | )
94 | }
95 | else -> throw UnsupportedOperationException("Unknown Uri: $uri")
96 | }
97 |
98 | // Set the notification URI for the cursor to the one passed into the function. This
99 | // causes the cursor to register a content observer to watch for changes that happen to
100 | // this URI and any of it's descendants. By descendants, we mean any URI that begins
101 | // with this path.
102 | val context = context
103 | if (context != null) {
104 | rCursor.setNotificationUri(context.contentResolver, uri)
105 | }
106 | return rCursor
107 | }
108 |
109 | /**
110 | * Determine the MIME type of the provided URI.
111 | * @param uri The URI.
112 | * @return The MIME type.
113 | */
114 | override fun getType(uri: Uri): String? {
115 | return when (sUriMatcher.match(uri)) {
116 | SEARCH_HISTORY -> HistoryContract.HistoryEntry.CONTENT_TYPE
117 | SEARCH_HISTORY_DATE, SEARCH_HISTORY_ID, SEARCH_HISTORY_IS_HISTORY -> HistoryContract.HistoryEntry.CONTENT_ITEM
118 | else -> throw UnsupportedOperationException("Uknown Uri: $uri")
119 | }
120 | }
121 |
122 | override fun insert(uri: Uri, values: ContentValues?): Uri? {
123 | val db = mOpenHelper.writableDatabase
124 | val _id: Long
125 | val retUri: Uri
126 | when (sUriMatcher.match(uri)) {
127 | SEARCH_HISTORY -> {
128 | _id = db.insert(HistoryContract.HistoryEntry.TABLE_NAME, null, values)
129 | retUri = if (_id > 0) {
130 | buildHistoryUri(_id)
131 | } else {
132 | throw UnsupportedOperationException("Unable to insert rows into: $uri")
133 | }
134 | }
135 | else -> throw UnsupportedOperationException("Unknown uri: $uri")
136 | }
137 | return retUri
138 | }
139 |
140 | override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int {
141 | val db = mOpenHelper.writableDatabase
142 | val rows: Int
143 | rows = when (sUriMatcher.match(uri)) {
144 | SEARCH_HISTORY -> db.update(HistoryContract.HistoryEntry.TABLE_NAME, values, selection, selectionArgs)
145 | else -> throw UnsupportedOperationException("Unknown uri: $uri")
146 | }
147 | if (rows != 0) {
148 | val context = context
149 | context?.contentResolver?.notifyChange(uri, null)
150 | }
151 | return rows
152 | }
153 |
154 | override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int {
155 | val db = mOpenHelper.writableDatabase
156 | // Number of rows effected.
157 | val rows: Int
158 | rows = when (sUriMatcher.match(uri)) {
159 | SEARCH_HISTORY -> db.delete(HistoryContract.HistoryEntry.TABLE_NAME, selection, selectionArgs)
160 | else -> throw UnsupportedOperationException("Unknown uri: $uri")
161 | }
162 | if (selection == null || rows != 0) {
163 | val context = context
164 | context?.contentResolver?.notifyChange(uri, null)
165 | }
166 | return rows
167 | }
168 | }
--------------------------------------------------------------------------------
/library/src/main/java/br/com/mauker/materialsearchview/utils/AnimationUtils.kt:
--------------------------------------------------------------------------------
1 | package br.com.mauker.materialsearchview.utils
2 |
3 | import android.animation.AnimatorListenerAdapter
4 | import android.annotation.TargetApi
5 | import android.view.View
6 | import android.view.ViewAnimationUtils
7 | import androidx.core.view.ViewCompat
8 | import androidx.core.view.ViewPropertyAnimatorListener
9 |
10 | /**
11 | * Created by Mauker on 18/04/2016. (dd/MM/yyyy).
12 | *
13 | * Utility class used to easily animate Views. Most used for revealing or hiding Views.
14 | */
15 | object AnimationUtils {
16 |
17 | @JvmStatic
18 | val ANIMATION_DURATION_SHORTEST = 150
19 |
20 | @JvmStatic
21 | val ANIMATION_DURATION_SHORT = 250
22 |
23 | @JvmStatic
24 | val ANIMATION_DURATION_MEDIUM = 400
25 |
26 | @JvmStatic
27 | val ANIMATION_DURATION_LONG = 800
28 |
29 | @TargetApi(21)
30 | @JvmStatic
31 | @JvmOverloads
32 | fun circleRevealView(view: View, duration: Int = ANIMATION_DURATION_SHORT) {
33 | // get the center for the clipping circle
34 | val cx = view.width
35 | val cy = view.height / 2
36 |
37 | // get the final radius for the clipping circle
38 | val finalRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()
39 |
40 | // create the animator for this view (the start radius is zero)
41 | val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius)
42 |
43 | anim.duration = if (duration > 0) duration.toLong() else ANIMATION_DURATION_SHORT.toLong()
44 |
45 | // make the view visible and start the animation
46 | view.visibility = View.VISIBLE
47 | anim.start()
48 | }
49 |
50 | @TargetApi(21)
51 | @JvmStatic
52 | fun circleHideView(view: View, listenerAdapter: AnimatorListenerAdapter) {
53 | circleHideView(view, ANIMATION_DURATION_SHORT, listenerAdapter)
54 | }
55 |
56 | @TargetApi(21)
57 | @JvmStatic
58 | fun circleHideView(view: View, duration: Int, listenerAdapter: AnimatorListenerAdapter) {
59 | // get the center for the clipping circle
60 | val cx = view.width
61 | val cy = view.height / 2
62 |
63 | // get the initial radius for the clipping circle
64 | val initialRadius = Math.hypot(cx.toDouble(), cy.toDouble()).toFloat()
65 |
66 | // create the animation (the final radius is zero)
67 | val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius, 0f)
68 |
69 | // make the view invisible when the animation is done
70 | anim.addListener(listenerAdapter)
71 |
72 | anim.duration = if (duration > 0) duration.toLong() else ANIMATION_DURATION_SHORT.toLong()
73 |
74 | // start the animation
75 | anim.start()
76 | }
77 |
78 | /**
79 | * Reveal the provided View with a fade-in animation.
80 | *
81 | * @param view The View that's being animated.
82 | * @param duration How long should the animation take, in millis.
83 | */
84 | @JvmStatic
85 | @JvmOverloads
86 | fun fadeInView(view: View, duration: Int = ANIMATION_DURATION_SHORTEST) {
87 | view.visibility = View.VISIBLE
88 | view.alpha = 0f
89 |
90 | // Setting the listener to null, so it won't keep getting called.
91 | ViewCompat.animate(view).alpha(1f).setDuration(duration.toLong()).setListener(null)
92 | }
93 |
94 | /**
95 | * Hide the provided View with a fade-out animation.
96 | *
97 | * @param view The View that's being animated.
98 | * @param duration How long should the animation take, in millis.
99 | */
100 | @JvmStatic
101 | @JvmOverloads
102 | fun fadeOutView(view: View, duration: Int = ANIMATION_DURATION_SHORTEST) {
103 | ViewCompat.animate(view).alpha(0f).setDuration(duration.toLong()).setListener(object : ViewPropertyAnimatorListener {
104 | override fun onAnimationStart(view: View) {
105 | view.isDrawingCacheEnabled = true
106 | }
107 |
108 | override fun onAnimationEnd(view: View) {
109 | view.visibility = View.GONE
110 | view.alpha = 1f
111 | view.isDrawingCacheEnabled = false
112 | }
113 |
114 | override fun onAnimationCancel(view: View) {}
115 | })
116 | }
117 |
118 | /**
119 | * Cross animate two views, showing one, hiding the other.
120 | *
121 | * @param showView The View that's going to be visible after the animation.
122 | * @param hideView The View that's going to disappear after the animation.
123 | */
124 | @JvmStatic
125 | @JvmOverloads
126 | fun crossFadeViews(showView: View, hideView: View, duration: Int = ANIMATION_DURATION_SHORT) {
127 | fadeInView(showView, duration)
128 | fadeOutView(hideView, duration)
129 | }
130 |
131 | // TODO - Cross fade with circle reveal.
132 | }
133 |
134 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_action_navigation_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_action_navigation_close.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_action_search_white.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_action_voice_search.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/drawable/ic_history_white.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/list_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
26 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/search_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
20 |
21 |
28 |
29 |
38 |
39 |
56 |
57 |
66 |
67 |
77 |
78 |
79 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/library/src/main/res/values-bs/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Zdravo svete!
5 | Podešavanja
6 |
7 | Obriši istoriju
8 | Obriši predloge
9 | Obriši sve
10 |
11 |
12 | - Jabuka
13 | - Kajsija
14 | - Avokado
15 | - Banana
16 | - Borovnica
17 | - Kupina
18 | - Šargarepa
19 | - Trešnja
20 | - Narandža
21 | - Krompir
22 | - Breskva
23 | - Marakuje
24 | - Kruška
25 | - Jagoda
26 |
27 |
--------------------------------------------------------------------------------
/library/src/main/res/values-es/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Buscar…
3 | Di un nombre o número
4 |
5 | Navegar hacia arriba
6 | Búsqueda por voz
7 | Borrar
8 | Ícono de elemento de lista
9 |
--------------------------------------------------------------------------------
/library/src/main/res/values-fr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Rechercher…
3 | Prononcer un nom ou un nombre
4 |
5 | Bouton de navigation UP
6 | Recherche vocale
7 | Effacer le champ
8 | Lister les icônes
9 |
10 |
--------------------------------------------------------------------------------
/library/src/main/res/values-hr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Zdravo svete!
5 | Podešavanja
6 |
7 | Obriši istoriju
8 | Obriši predloge
9 | Obriši sve
10 |
11 |
12 | - Jabuka
13 | - Kajsija
14 | - Avokado
15 | - Banana
16 | - Borovnica
17 | - Kupina
18 | - Šargarepa
19 | - Trešnja
20 | - Narandža
21 | - Krompir
22 | - Breskva
23 | - Marakuje
24 | - Kruška
25 | - Jagoda
26 |
27 |
--------------------------------------------------------------------------------
/library/src/main/res/values-it/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Cerca…
3 | Pronuncia un nome o un numero
4 |
5 | Tasto navigazione SU
6 | Ricerca vocale
7 | Svuota campo
8 |
9 |
--------------------------------------------------------------------------------
/library/src/main/res/values-nl/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Zoek…
4 | Spreek een item naam of een nummer
5 |
6 | Navigatie omhoog knop
7 | Spraaksgewijs zoeken
8 | Veld legen
9 | Lijst item icoon
10 |
11 |
--------------------------------------------------------------------------------
/library/src/main/res/values-pt-rBR/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Buscar…
4 | Fale um nome ou número
5 | Botão de navegação UP
6 | Busca por voz
7 | Limpar campo
8 | Ícone do item da lista
9 |
--------------------------------------------------------------------------------
/library/src/main/res/values-sr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Zdravo svete!
5 | Podešavanja
6 |
7 | Obriši istoriju
8 | Obriši predloge
9 | Obriši sve
10 |
11 |
12 | - Jabuka
13 | - Kajsija
14 | - Avokado
15 | - Banana
16 | - Borovnica
17 | - Kupina
18 | - Šargarepa
19 | - Trešnja
20 | - Narandža
21 | - Krompir
22 | - Breskva
23 | - Marakuje
24 | - Kruška
25 | - Jagoda
26 |
27 |
--------------------------------------------------------------------------------
/library/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 |
--------------------------------------------------------------------------------
/library/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | @color/gray
4 | @color/white_ish
5 |
6 | #00FFFFFF
7 |
8 | #ffffffff
9 | #ff000000
10 | #ff888888
11 |
12 | #88ffffff
13 | #88000000
14 | #88888888
15 |
16 | #fffefefe
17 |
--------------------------------------------------------------------------------
/library/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 12dp
4 | 8dp
5 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Material Search Library
3 | Search…
4 | Speak an item name or a number
5 |
6 | Up navigation button
7 | Voice search
8 | Clear field
9 | List item icon
10 |
11 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
--------------------------------------------------------------------------------