aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-08-05 23:10:28 -0700
committerGitHub <noreply@github.com>2017-08-05 23:10:28 -0700
commit187d8e64dc7189f63707d154166867084662dbe3 (patch)
tree372503ac381f12a905a0608519228f9792bb1c0b
parentcaaa5653deda0640a475d0ccad6daeb7852502f7 (diff)
downloadkau-187d8e64dc7189f63707d154166867084662dbe3.tar.gz
kau-187d8e64dc7189f63707d154166867084662dbe3.tar.bz2
kau-187d8e64dc7189f63707d154166867084662dbe3.zip
Create debounce and update searchview (#27)
* Prepare version * Create debounce base * Add debouncer and fix transition crash * Add debounce docs * Update links * Update searchview docs * Test without a ref * Add links to core components * Update links * Update to bullet points * Test core md * Test slash * Test slash * Specify implemented dependencies
-rw-r--r--README.md27
-rw-r--r--core/README.md35
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kotlin/Debouncer.kt124
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt2
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt3
-rw-r--r--core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt53
-rw-r--r--docs/Changelog.md7
-rw-r--r--gradle.properties2
-rw-r--r--sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt17
-rw-r--r--sample/src/main/res/xml/kau_changelog.xml10
-rw-r--r--searchview/README.md3
-rw-r--r--searchview/build.gradle4
-rw-r--r--searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt164
13 files changed, 329 insertions, 122 deletions
diff --git a/README.md b/README.md
index 17a486d..5092d77 100644
--- a/README.md
+++ b/README.md
@@ -57,11 +57,25 @@ dependencies {
# Submodules
> Linked to their respective docs.<br/>
-> Included dependencies are only those with exposed APIs; see [new dependency configurations](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).
+> Included dependencies are only those with exposed APs; see [new dependency configurations](https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#new_configurations).<br/>
+> Implemented dependencies are wrapped in parentheses.
## [Core](core#readme)
* Collection of extension functions and small helper methods applicable in almost any application.
-* Notable features: KPrefs, Changelog XML, Kotterknife, Ripple Canvas, Delegates, Swipe, Lazy Resettables, Extensions, Email Builder
+* Notable features:
+ * [KPrefs](core#kprefs)
+ * [Changelog XML](core#changelog-xml)
+ * [FAQ XML](core#faq-xml)
+ * [Kotterknife](core#kotterknife)
+ * [Ripple Canvas](core#ripple-canvas)
+ * [MeasureSpecDelegate](core#measure-spec-delegate)
+ * [CollapsibleViewDelegate](core#collapsible-view-delegate)
+ * [Swipe](core#swipe)
+ * [Debounce](core#debounce)
+ * [Timber Logger](core#timber-logger)
+ * [Email Builder](core#email-builder)
+ * [Extension Functions](core#extension-functions)
+ * [Lazy Resettable](core#lazy-resettable)
* Includes
[`AppCompat`](https://developer.android.com/topic/libraries/support-library/index.html),
[`Material Dialogs (core)`](https://github.com/afollestad/material-dialogs),
@@ -87,7 +101,7 @@ dependencies {
## [Color Picker](colorpicker#readme)
* Implementation of a color picker dialog with subtle transitions and a decoupled callback
* Includes `:core`,
-[`Material Dialogs (commons)`](https://github.com/afollestad/material-dialogs)
+([`Material Dialogs (commons)`](https://github.com/afollestad/material-dialogs))
## [KPref Activity](kpref-activity#readme)
* Fully programmatic implementation of a Preference Activity, backed by RecyclerViews
@@ -97,14 +111,11 @@ dependencies {
* Fully functional image and video pickers, both as an overlay and as a requested activity.
* Includes `:core-ui`,
[`Glide`](https://github.com/bumptech/glide),
-[`Blurry`](https://github.com/wasabeef/Blurry)
+([`Blurry`](https://github.com/wasabeef/Blurry))
## [SearchView](searchview#readme)
* Material searchview with kotlin bindings
-* Includes `:core-ui`, `:adapter`,
-[`RxAndroid`](https://github.com/ReactiveX/RxAndroid),
-[`RxKotlin`](https://github.com/ReactiveX/RxKotlin),
-[`RxBinding`](https://github.com/JakeWharton/RxBinding)
+* Includes `:core-ui`, `:adapter`
-----------
diff --git a/core/README.md b/core/README.md
index 2c8ae9d..6796082 100644
--- a/core/README.md
+++ b/core/README.md
@@ -5,19 +5,19 @@
## Contents
* [KPrefs](#kprefs)
-* [Changelog XML](#changelog)
+* [Changelog XML](#changelog-xml)
* [FAQ XML](#faq-xml)
* [Kotterknife](#kotterknife)
* [Ripple Canvas](#ripple-canvas)
* [MeasureSpecDelegate](#measure-spec-delegate)
* [CollapsibleViewDelegate](#collapsible-view-delegate)
* [Swipe](#swipe)
+* [Debounce](#debounce)
* [Timber Logger](#timber-logger)
* [Email Builder](#email-builder)
-* [Extensions](#extensions)
+* [Extension Functions](#extension-functions)
* [Lazy Resettable](#lazy-resettable)
-<a name="kprefs"></a>
## KPrefs
A typical SharedPreference contains items that look like so:
@@ -71,7 +71,6 @@ object MyPrefs : KPref() {
Notice that it is a `val` and takes no default. It will return true the first time and false for all subsequent calls.
-<a name="changelog"></a>
## Changelog XML
Create an xml resource with the following structure:
@@ -109,7 +108,6 @@ Here is a template xml changelog file:
</resources>
```
-<a name="faq-xml"></a>
## FAQ XML
There is another parser for a FAQ list with the following format:
@@ -123,7 +121,6 @@ Call `kauParseFaq` and pass a callback taking in a `List<Pair<Spanned, Spanned>`
By default, the questions are numbered, and the content is formatted with HTML.
You may still need to add your own methods to allow interaction with certain elements such as links.
-<a name="kotterknife"></a>
## Kotterknife
KAU comes shipped with [Kotterknife](https://github.com/JakeWharton/kotterknife) by Jake Wharton.
@@ -134,7 +131,6 @@ These variants are weakly held in the private `KotterknifeRegistry` object, and
values through the `Kotterknife.reset` method. This is typically useful for Fragments, as they do not follow
the same lifecycle as Activities and Views.
-<a name="ripple-canvas"></a>
## Ripple Canvas
Ripple canvas provides a way to create simultaneous ripples against a background color.
@@ -145,21 +141,18 @@ They can be used as transitions, or as a toolbar background to replicate the loo
Many ripples can be stacked on top of each other to run at the same time from different locations.
The canvas also supports color fading and direct color setting so it can effectively replace any background.
-<a name="measure-spec-delegate"></a>
## Measure Spec Delegate
If you ever have a view needing exact aspect ratios with its parent and/or itself, this delegate is here to help.
Implementing this in any view class unlocks its attributes, giving you three layers of view measuring to ensure exact sizing.
More information can be found in the [klass file](https://github.com/AllanWang/KAU/blob/master/core/src/main/kotlin/ca/allanwang/kau/ui/views/MeasureSpecDelegate.kt)
-<a name="collapsible-view-delegate"></a>
## Collapsible View Delegate
A common animation is having a view that can smoothly enter and exit by changing its height.
This delegate will implement everything for you and give you the methods `expand`, `collapse`, etc.
See the [kclass file](https://github.com/AllanWang/KAU/blob/master/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt) for more details.
-<a name="swipe"></a>
# Swipe
A collection of activity extension methods to easily make any activity swipable:
@@ -182,20 +175,35 @@ Special thanks goes to the original project, [SwipeBackHelper](https://github.co
KAU's swipe is a Kotlin rewrite, along with support for all directions and weakly referenced contexts.
-<a name="timber-logger"></a>
+# Debounce
+
+Debouncing is a means of throttling a function so that it is called no more than once in a given instance of time.
+An example where you'd like this behaviour is the searchview; you want to deliver search results quickly,
+but you don't want to update your response with each new character.
+Instead, you can wait until a user finishes their query, then search for the results.
+
+Example:
+
+![Debounce 0](https://raw.githubusercontent.com/AllanWang/Storage-Hub/master/kau/kau_search_debounce_0.gif)
+![Debounce 500](https://raw.githubusercontent.com/AllanWang/Storage-Hub/master/kau/kau_search_debounce_500.gif)
+
+The first case is an example of no debouncing, whereas the second case is an example with a 500ms debounce.
+
+KAU offers extensions to easily convert or create functions into debouncables.
+Simply call `debounce` and specify your interval on an existing function, or with a new function.
+
+
## Timber Logger
[Timber](https://github.com/JakeWharton/timber)'s DebugTree uses the tag to specify the current class that is being logged.
To add the tag directly in the message, create an object that extends the TimberLogger class with the tag name as the argument.
Along with the timber methods (`v`, `i`, `d`, `e`), Timber Logger also supports `eThrow` to wrap a String in a throwable
-<a name="email-builder"></a>
## Email Builder
Easily send an email through `Context.sendEmail`.
Include your email and subject, along with other optional configurations such as retrieving device info.
-<a name="extensions"></a>
## Extension Functions
> "[Extensions](https://kotlinlang.org/docs/reference/extensions.html) provide the ability to extend a class with new functionality without having to inherit from the class"
@@ -204,7 +212,6 @@ Note that since KAU depends on [ANKO](https://github.com/Kotlin/anko), all of th
KAU's vast collection of extensions is one of its strongest features.
There are too many to explain here, but you may check out the [utils package](https://github.com/AllanWang/KAU/tree/master/core/src/main/kotlin/ca/allanwang/kau/utils)
-<a name="lazy-resettable></a>
## Lazy Resettable
In the spirit of Kotlin's Lazy delegate, KAU supports a resettable version. Calling `lazyResettable` produces the same delegate,
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/Debouncer.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/Debouncer.kt
new file mode 100644
index 0000000..4fba2c8
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/Debouncer.kt
@@ -0,0 +1,124 @@
+package ca.allanwang.kau.kotlin
+
+import ca.allanwang.kau.logging.KL
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+
+/**
+ * Created by Allan Wang on 2017-08-05.
+ *
+ * Thread safe function wrapper to allow for debouncing
+ * With reference to <a href="https://stackoverflow.com/a/20978973/4407321">Stack Overflow</a>
+ */
+
+/**
+ * The debouncer base
+ * Implements everything except for the callback,
+ * as the number of variables is different between implementations
+ * You may still use this without extending it, but you'll have to pass a callback each time
+ */
+open class Debouncer(var interval: Long) {
+ private val sched = Executors.newScheduledThreadPool(1)
+ private var task: DebounceTask? = null
+
+ /**
+ * Generic invocation to pass a callback to the new task
+ * Pass a new callback for the task
+ * If another task is pending, it will be invalidated
+ */
+ operator fun invoke(callback: () -> Unit) {
+ synchronized(this) {
+ task?.invalidate()
+ val newTask = DebounceTask(callback)
+ KL.v("Debouncer task created: $newTask in $this")
+ sched.schedule(newTask, interval, TimeUnit.MILLISECONDS)
+ task = newTask
+ }
+ }
+
+ /**
+ * Call to cancel all pending requests and shutdown the thread pool
+ * The debouncer cannot be used after this
+ */
+ fun terminate() = sched.shutdownNow()
+
+ /**
+ * Invalidate any pending tasks
+ */
+ fun cancel() {
+ synchronized(this) {
+ if (task != null) KL.v("Debouncer cancelled for $task in $this")
+ task?.invalidate()
+ task = null
+ }
+ }
+
+}
+
+/*
+ * Helper extensions for functions with 0 to 3 arguments
+ */
+
+/**
+ * The debounced task
+ * Holds a callback to execute if the time has come and it is still valid
+ * All methods can be viewed as synchronous as the invocation is synchronous
+ */
+private class DebounceTask(inline val callback: () -> Unit) : Runnable {
+ private var valid = true
+
+ fun invalidate() {
+ valid = false
+ }
+
+ override fun run() {
+ if (!valid) return
+ valid = false
+ KL.v("Debouncer task executed $this")
+ try {
+ callback()
+ } catch (e: Exception) {
+ KL.e(e, "DebouncerTask exception")
+ }
+ }
+}
+
+/**
+ * A zero input debouncer
+ */
+class Debouncer0 internal constructor(interval: Long, val callback: () -> Unit) : Debouncer(interval) {
+ operator fun invoke() = invoke(callback)
+}
+
+fun debounce(interval: Long, callback: () -> Unit) = Debouncer0(interval, callback)
+fun (() -> Unit).debounce(interval: Long) = debounce(interval, this)
+
+/**
+ * A one argument input debouncer
+ */
+class Debouncer1<T> internal constructor(interval: Long, val callback: (T) -> Unit) : Debouncer(interval) {
+ operator fun invoke(key: T) = invoke { callback(key) }
+}
+
+fun <T> debounce(interval: Long, callback: (T) -> Unit) = Debouncer1(interval, callback)
+fun <T> ((T) -> Unit).debounce(interval: Long) = debounce(interval, this)
+
+/**
+ * A two argument input debouncer
+ */
+class Debouncer2<T, V> internal constructor(interval: Long, val callback: (T, V) -> Unit) : Debouncer(interval) {
+ operator fun invoke(arg0: T, arg1: V) = invoke { callback(arg0, arg1) }
+}
+
+fun <T, V> debounce(interval: Long, callback: (T, V) -> Unit) = Debouncer2(interval, callback)
+fun <T, V> ((T, V) -> Unit).debounce(interval: Long) = debounce(interval, this)
+
+/**
+ * A three argument input debouncer
+ */
+class Debouncer3<T, U, V> internal constructor(interval: Long, val callback: (T, U, V) -> Unit) : Debouncer(interval) {
+ operator fun invoke(arg0: T, arg1: U, arg2: V) = invoke { callback(arg0, arg1, arg2) }
+}
+
+fun <T, U, V> debounce(interval: Long, callback: ((T, U, V) -> Unit)) = Debouncer3(interval, callback)
+fun <T, U, V> ((T, U, V) -> Unit).debounce(interval: Long) = debounce(interval, this)
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
index 2ac5d2f..ceeaa30 100644
--- a/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
+++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
@@ -13,7 +13,7 @@ internal object UNINITIALIZED
fun <T : Any> lazyResettable(initializer: () -> T): LazyResettable<T> = LazyResettable<T>(initializer)
-open class LazyResettable<T : Any>(private val initializer: () -> T, lock: Any? = null) : ILazyResettable<T>, Serializable {
+class LazyResettable<T : Any>(private val initializer: () -> T, lock: Any? = null) : ILazyResettable<T>, Serializable {
@Volatile private var _value: Any = UNINITIALIZED
private val lock = lock ?: this
diff --git a/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt b/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
index 4c6d655..4cf566a 100644
--- a/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
+++ b/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
@@ -18,4 +18,7 @@ open class TimberLogger(tag: String) {
inline fun i(s: String) = Timber.i(TAG, s)
inline fun v(s: String) = Timber.v(TAG, s)
inline fun eThrow(s: String) = e(Throwable(s))
+// fun plant() {
+// Timber.plant(Timber.Tree())
+// }
} \ No newline at end of file
diff --git a/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt b/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt
new file mode 100644
index 0000000..12bc5a4
--- /dev/null
+++ b/core/src/test/kotlin/ca/allanwang/kau/kotlin/DebounceTest.kt
@@ -0,0 +1,53 @@
+package ca.allanwang.kau.kotlin
+
+import org.jetbrains.anko.doAsync
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * Created by Allan Wang on 2017-08-05.
+ */
+class DebounceTest {
+
+ @Test
+ fun basic() {
+ var i = 0
+ val debounce = debounce(20) { i++ }
+ assertEquals(0, i, "i should start as 0")
+ (1..5).forEach { debounce() }
+ Thread.sleep(50)
+ assertEquals(1, i, "Debouncing did not cancel previous requests")
+ }
+
+ @Test
+ fun basicExtension() {
+ var i = 0
+ val increment: () -> Unit = { i++ }
+ (1..5).forEach { increment() }
+ assertEquals(5, i, "i should be 5")
+ val debounce = increment.debounce(50)
+ (6..10).forEach { debounce() }
+ assertEquals(5, i, "i should not have changed")
+ Thread.sleep(100)
+ assertEquals(6, i, "i should increment to 6")
+ }
+
+ @Test
+ fun multipleDebounces() {
+ var i = 0
+ val debounce = debounce<Int>(10) { i += it }
+ debounce(1) //ignore -> i = 0
+ Thread.sleep(5)
+ assertEquals(0, i)
+ debounce(2) //accept -> i = 2
+ Thread.sleep(15)
+ assertEquals(2, i)
+ debounce(4) //ignore -> i = 2
+ Thread.sleep(5)
+ assertEquals(2, i)
+ debounce(8) //accept -> i = 10
+ Thread.sleep(15)
+ assertEquals(10, i)
+ }
+
+} \ No newline at end of file
diff --git a/docs/Changelog.md b/docs/Changelog.md
index 5d6c743..3c61dd3 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -1,11 +1,16 @@
# Changelog
-## v3.2.4
+## v3.3.0
+* :core: Create debounce methods
+* :searchview: [Breaking] remove reactive dependencies and stick with basic callbacks
+
+## v3.2.5
* :core: Fix FAQ background
* :core: Create FileUtils
* :core: Create NotificationUtils
* :core: Update swipe to remove most exceptions
* :core: Make logging class functions inline
+* :core: Create removeIf for mutableIteratables
* :core-ui: Move reactive libs to :searchview:
## v3.2.3
diff --git a/gradle.properties b/gradle.properties
index 8d65cac..a778a66 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -23,7 +23,7 @@ TARGET_SDK=26
BUILD_TOOLS=26.0.1
ANDROID_SUPPORT_LIBS=26.0.0
-VERSION_NAME=3.2.5
+VERSION_NAME=3.2.6
KOTLIN=1.1.3-2
ABOUT_LIBRARIES=5.9.7
diff --git a/sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt b/sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt
index 51b8530..93fc651 100644
--- a/sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt
+++ b/sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt
@@ -224,18 +224,13 @@ class MainActivity : KPrefActivity() {
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_main, menu)
if (searchView == null) searchView = bindSearchView(menu, R.id.action_search) {
- textObserver = {
- observable, searchView ->
- /*
- * Notice that this function is automatically executed in a new thread
- * and that the results will automatically be set on the ui thread
- */
- observable.subscribe {
- text ->
- val items = wordBank.filter { it.contains(text) }.sorted().map { SearchItem(it) }
- searchView.results = items
- }
+
+ textCallback = {
+ query, searchView ->
+ val items = wordBank.filter { it.contains(query) }.sorted().map { SearchItem(it) }
+ searchView.results = items
}
+ textDebounceInterval = 0
noResultsFound = R.string.kau_no_results_found
shouldClearOnClose = false
onItemClick = {
diff --git a/sample/src/main/res/xml/kau_changelog.xml b/sample/src/main/res/xml/kau_changelog.xml
index c11353d..96ae965 100644
--- a/sample/src/main/res/xml/kau_changelog.xml
+++ b/sample/src/main/res/xml/kau_changelog.xml
@@ -6,7 +6,13 @@
<item text="" />
-->
- <version title="v3.2.4"/>
+ <version title="v3.3.0"/>
+ <item text=":core: Create debounce methods" />
+ <item text=":searchview: [Breaking] remove reactive dependencies and stick with basic callbacks" />
+ <item text="" />
+ <item text="" />
+
+ <version title="v3.2.5"/>
<item text=":core: Fix FAQ background" />
<item text=":core: Create FileUtils" />
<item text=":core: Create NotificationUtils" />
@@ -14,8 +20,6 @@
<item text=":core: Make logging class functions inline" />
<item text=":core: Create removeIf for mutableIteratables" />
<item text=":core-ui: Move reactive libs to :searchview:" />
- <item text="" />
- <item text="" />
<version title="v3.2.3"/>
<item text=":about: Modularize everything" />
diff --git a/searchview/README.md b/searchview/README.md
index d8e690b..37d059a 100644
--- a/searchview/README.md
+++ b/searchview/README.md
@@ -9,5 +9,4 @@ The searchview is:
* Fully themable - set the foreground or background color to style every portion, from text colors to backgrounds to ripples
* Complete - binding the search view to a menu id will set the menu icon (if not previously set) and attach all the necessary listeners
* Configurable - modify any portion of the inner Config class when binding the search view
-* Thread friendly - the search view is built with observables and emits values in a separate thread,
-which means that you don't have to worry about long processes in the text watcher. Likewise, all adapter changes are automatically done on the ui thread.
+* Debouncable - specify a time interval to throttle your queries; see [debouncing](/core#debounce) \ No newline at end of file
diff --git a/searchview/build.gradle b/searchview/build.gradle
index 0f691b0..642d11a 100644
--- a/searchview/build.gradle
+++ b/searchview/build.gradle
@@ -5,10 +5,6 @@ apply from: '../android-lib.gradle'
dependencies {
compile project(':core-ui')
compile project(':adapter')
-
- compile "io.reactivex.rxjava2:rxkotlin:${RX_KOTLIN}"
- compile "io.reactivex.rxjava2:rxandroid:${RX_ANDROID}"
- compile "com.jakewharton.rxbinding2:rxbinding-appcompat-v7-kotlin:${RX_BINDING}"
}
apply from: '../artifacts.gradle'
diff --git a/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
index 4058f16..a98b2f6 100644
--- a/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
+++ b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
@@ -6,33 +6,35 @@ import android.content.res.ColorStateList
import android.graphics.Color
import android.support.annotation.ColorInt
import android.support.annotation.IdRes
-import android.support.annotation.StringRes
-import android.support.transition.AutoTransition
+import android.support.transition.ChangeBounds
+import android.support.transition.TransitionManager
+import android.support.transition.TransitionSet
import android.support.v7.widget.AppCompatEditText
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
+import android.text.Editable
+import android.text.TextWatcher
import android.util.AttributeSet
import android.view.*
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.ProgressBar
-import ca.allanwang.kau.kotlin.nonReadable
+import ca.allanwang.kau.kotlin.Debouncer2
+import ca.allanwang.kau.kotlin.debounce
+import ca.allanwang.kau.logging.KL
import ca.allanwang.kau.searchview.SearchView.Configs
import ca.allanwang.kau.ui.views.BoundedCardView
import ca.allanwang.kau.utils.*
-import com.jakewharton.rxbinding2.widget.RxTextView
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
-import io.reactivex.Observable
-import io.reactivex.schedulers.Schedulers
import org.jetbrains.anko.runOnUiThread
/**
* Created by Allan Wang on 2017-06-23.
*
- * A materialized SearchView with complete theming and observables
+ * A materialized SearchView with complete theming and customization
* This view can be added programmatically and configured using the [Configs] DSL
* It is preferred to add the view through an activity, but it can be attached to any ViewGroup
* Beware of where specifically this is added, as its view or the keyboard may affect positioning
@@ -46,63 +48,34 @@ class SearchView @JvmOverloads constructor(
/**
* Collection of all possible arguments when building the SearchView
- * Everything is made as opened as possible so other components may be found in the [SearchView]
- * However, these are the notable options put together an an inner class for better visibility
+ * Everything is made as opened as possible, so additional components may be found in the [SearchView]
+ * However, these are the main config options
*/
- inner class Configs {
+ class Configs {
/**
- * In the searchview, foreground color accounts for all text colors and icon colors
+ * The foreground color accounts for all text colors and icon colors
* Various alpha levels may be used for sub texts/dividers etc
*/
- var foregroundColor: Int
- get() = SearchItem.foregroundColor
- set(value) {
- if (SearchItem.foregroundColor == value) return
- SearchItem.foregroundColor = value
- tintForeground(value)
- }
+ var foregroundColor: Int = SearchItem.foregroundColor
/**
* Namely the background for the card and recycler view
*/
- var backgroundColor: Int
- get() = SearchItem.backgroundColor
- set(value) {
- if (SearchItem.backgroundColor == value) return
- SearchItem.backgroundColor = value
- tintBackground(value)
- }
+ var backgroundColor: Int = SearchItem.backgroundColor
/**
* Icon for the leftmost ImageView, which typically contains the hamburger menu/back arror
*/
var navIcon: IIcon? = GoogleMaterial.Icon.gmd_arrow_back
- set(value) {
- field = value
- iconNav.setSearchIcon(value)
- if (value == null) iconNav.gone()
- }
-
/**
* Optional icon just to the left of the clear icon
* This is not implemented by default, but can be used for anything, such as mic or redirects
* Returns the extra imageview
* Set the iicon as null to hide the extra icon
*/
- fun setExtraIcon(iicon: IIcon?, onClick: OnClickListener?): ImageView {
- iconExtra.setSearchIcon(iicon)
- if (iicon == null) iconClear.gone()
- iconExtra.setOnClickListener(onClick)
- return iconExtra
- }
-
+ var extraIcon: Pair<IIcon, OnClickListener>? = null
/**
* Icon for the rightmost ImageView, which typically contains a close icon
*/
var clearIcon: IIcon? = GoogleMaterial.Icon.gmd_clear
- set(value) {
- field = value
- iconClear.setSearchIcon(value)
- if (value == null) iconClear.gone()
- }
/**
* Duration for the circular reveal animation
*/
@@ -128,27 +101,14 @@ class SearchView @JvmOverloads constructor(
* The divider is colored based on the [foregroundColor]
*/
var withDivider: Boolean = true
- set(value) {
- field = value
- if (value) divider.visible() else divider.invisible()
- }
/**
* Hint string to be set in the searchView
*/
- var hintText: String?
- get() = editText.hint?.toString()
- set(value) {
- editText.hint = value
- }
+ var hintText: String? = null
/**
* Hint string res to be set in the searchView
*/
- var hintTextRes: Int
- @Deprecated(level = DeprecationLevel.ERROR, message = "Non readable property")
- get() = nonReadable()
- @StringRes set(value) {
- hintText = context.string(value)
- }
+ var hintTextRes: Int = -1
/**
* StringRes for a "no results found" item
* If [results] is ever set to an empty list, it will default to
@@ -159,11 +119,13 @@ class SearchView @JvmOverloads constructor(
*/
var noResultsFound: Int = -1
/**
- * Text watcher configurations on init
- * By default, the observable is on a separate thread, so you may directly execute background processes
- * This builder acts on an observable, so you may switch threads, debounce, and do anything else that you require
+ * Callback for when the query changes
+ */
+ var textCallback: (query: String, searchView: SearchView) -> Unit = { _, _ -> }
+ /**
+ * Debouncing interval between callbacks
*/
- var textObserver: (observable: Observable<String>, searchView: SearchView) -> Unit = { _, _ -> }
+ var textDebounceInterval: Long = 0
/**
* Click event for suggestion items
* This event is only triggered when [key] is not blank (like in [noResultsFound]
@@ -179,6 +141,32 @@ class SearchView @JvmOverloads constructor(
* See [SearchItem.withHighlights]
*/
var highlightQueryText: Boolean = true
+
+ /**
+ * Sets config attributes to the given searchView
+ */
+ internal fun apply(searchView: SearchView) {
+ with(searchView) {
+ if (SearchItem.foregroundColor != foregroundColor) {
+ SearchItem.foregroundColor = foregroundColor
+ tintForeground(foregroundColor)
+ }
+ if (SearchItem.backgroundColor != backgroundColor) {
+ SearchItem.backgroundColor = backgroundColor
+ tintForeground(backgroundColor)
+ }
+ val icons = mutableListOf(navIcon to iconNav, clearIcon to iconClear)
+ val extra = extraIcon
+ if (extra != null) icons.add(extra.first to iconExtra)
+ icons.forEach { (iicon, view) -> view.goneIf(iicon == null).setSearchIcon(iicon) }
+
+ if (extra != null) iconExtra.setOnClickListener(extra.second)
+ divider.invisibleIf(!withDivider)
+ editText.hint = context.string(hintTextRes, hintText)
+ textCallback.terminate()
+ textCallback = debounce(textDebounceInterval, this@Configs.textCallback)
+ }
+ }
}
/**
@@ -200,7 +188,10 @@ class SearchView @JvmOverloads constructor(
* Empties the list on the UI thread
* The noResults item will not be added
*/
- internal fun clearResults() = context.runOnUiThread { cardTransition(); adapter.clear() }
+ internal fun clearResults() {
+ textCallback.cancel()
+ context.runOnUiThread { cardTransition(); adapter.clear() }
+ }
val configs = Configs()
//views
@@ -208,12 +199,13 @@ class SearchView @JvmOverloads constructor(
private val card: BoundedCardView by bindView(R.id.kau_search_cardview)
private val iconNav: ImageView by bindView(R.id.kau_search_nav)
private val editText: AppCompatEditText by bindView(R.id.kau_search_edit_text)
- val textEvents: Observable<String>
private val progress: ProgressBar by bindView(R.id.kau_search_progress)
- val iconExtra: ImageView by bindView(R.id.kau_search_extra)
+ private val iconExtra: ImageView by bindView(R.id.kau_search_extra)
private val iconClear: ImageView by bindView(R.id.kau_search_clear)
private val divider: View by bindView(R.id.kau_search_divider)
private val recycler: RecyclerView by bindView(R.id.kau_search_recycler)
+ private var textCallback: Debouncer2<String, SearchView>
+ = debounce(0) { query, _ -> KL.d("Search query $query found; set your own textCallback") }
val adapter = FastItemAdapter<SearchItem>()
var menuItem: MenuItem? = null
val isOpen: Boolean
@@ -256,12 +248,18 @@ class SearchView @JvmOverloads constructor(
if (item.key.isNotBlank()) configs.onItemLongClick(position, item.key, item.content, this@SearchView); true
}
}
- textEvents = RxTextView.textChangeEvents(editText)
- .skipInitialValue()
- .observeOn(Schedulers.newThread())
- .map { it.text().toString().trim() }
- textEvents.filter { it.isBlank() }
- .subscribe { clearResults() }
+ editText.addTextChangedListener(object : TextWatcher {
+ override fun afterTextChanged(s: Editable?) {}
+
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
+ val valid = !s.isNullOrBlank()
+ if (valid) textCallback(s.toString().trim(), this@SearchView)
+ else clearResults()
+ }
+
+ })
}
internal fun ImageView.setSearchIcon(iicon: IIcon?): ImageView {
@@ -269,12 +267,22 @@ class SearchView @JvmOverloads constructor(
return this
}
- internal fun cardTransition(builder: AutoTransition.() -> Unit = {}) {
- card.transitionAuto { duration = configs.transitionDuration; builder() }
+ internal fun cardTransition(builder: TransitionSet.() -> Unit = {}) {
+ TransitionManager.beginDelayedTransition(card,
+ //we are only using change bounds, as the recyclerview items may be animated as well,
+ //which causes a measure IllegalStateException
+ TransitionSet().addTransition(ChangeBounds()).apply {
+ duration = configs.transitionDuration
+ builder()
+ })
}
+ /**
+ * Update the base configurations and apply them to the searchView
+ */
fun config(config: Configs.() -> Unit) {
configs.config()
+ configs.apply(this)
}
/**
@@ -284,7 +292,6 @@ class SearchView @JvmOverloads constructor(
*/
fun bind(menu: Menu, @IdRes id: Int, @ColorInt menuIconColor: Int = Color.WHITE, config: Configs.() -> Unit = {}): SearchView {
config(config)
- configs.textObserver(textEvents.filter { it.isNotBlank() }, this)
menuItem = menu.findItem(id) ?: throw IllegalArgumentException("Menu item with given id doesn't exist")
if (menuItem!!.icon == null) menuItem!!.icon = GoogleMaterial.Icon.gmd_search.toDrawable(context, 18, menuIconColor)
card.gone()
@@ -293,14 +300,17 @@ class SearchView @JvmOverloads constructor(
return this
}
+ /**
+ * Call to remove the searchView from the original menuItem,
+ * with the option to replace the item click listener
+ */
fun unBind(replacementMenuItemClickListener: ((item: MenuItem) -> Boolean)? = null) {
parentViewGroup.removeView(this)
- if (replacementMenuItemClickListener != null)
- menuItem?.setOnMenuItemClickListener(replacementMenuItemClickListener)
+ menuItem?.setOnMenuItemClickListener(replacementMenuItemClickListener)
menuItem = null
}
- fun configureCoords(item: MenuItem) {
+ private fun configureCoords(item: MenuItem) {
val view = parentViewGroup.findViewById<View>(item.itemId) ?: return
val locations = IntArray(2)
view.getLocationOnScreen(locations)