From 6ad176dde0a84a0eb96dea2f9c7eb34394045526 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 24 Jun 2017 19:09:54 -0700 Subject: Push working build --- library/build.gradle | 5 + .../ca/allanwang/kau/searchview/SearchItem.kt | 16 +-- .../ca/allanwang/kau/searchview/SearchView.kt | 138 ++++++++++++++++----- .../kotlin/ca/allanwang/kau/utils/ActivityUtils.kt | 4 +- .../ca/allanwang/kau/utils/TransitionUtils.kt | 19 +++ .../kotlin/ca/allanwang/kau/utils/ViewUtils.kt | 14 ++- library/src/main/res/layout/kau_search_view.xml | 4 +- library/src/main/res/transition/kau_auto.xml | 4 + library/src/main/res/values/strings_commons.xml | 1 + 9 files changed, 160 insertions(+), 45 deletions(-) create mode 100644 library/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt create mode 100644 library/src/main/res/transition/kau_auto.xml (limited to 'library') diff --git a/library/build.gradle b/library/build.gradle index 3498378..946dbe2 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -62,6 +62,11 @@ dependencies { compile "com.jakewharton.timber:timber:${TIMBER}" compile "org.jetbrains.anko:anko-commons:${ANKO}" + + compile "io.reactivex.rxjava2:rxkotlin:${RX_JAVA}" + compile "io.reactivex.rxjava2:rxandroid:${RX_ANDROID}" + compile "com.jakewharton.rxbinding2:rxbinding-kotlin:${RX_BINDING}" + compile "com.jakewharton.rxbinding2:rxbinding-appcompat-v7-kotlin:${RX_BINDING}" } // build a jar with source files diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt index 0b36a5d..fac5ca1 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt @@ -5,9 +5,11 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import ca.allanwang.kau.R -import ca.allanwang.kau.utils.LazyResettable import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.setIcon import com.mikepenz.fastadapter.items.AbstractItem +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.mikepenz.iconics.typeface.IIcon /** * Created by Allan Wang on 2017-06-23. @@ -16,10 +18,11 @@ import com.mikepenz.fastadapter.items.AbstractItem * Contains a [key] which acts as a unique identifier (eg url) * and a [content] which is displayed in the item */ -class SearchItem(val key: String, val content: String = key) : AbstractItem() { +class SearchItem(val key: String, val content: String = key, val iicon: IIcon? = GoogleMaterial.Icon.gmd_search +) : AbstractItem() { companion object { - var foregroundColor: Int?=null + var foregroundColor: Int? = null } override fun getLayoutRes(): Int = R.layout.kau_search_item @@ -32,7 +35,7 @@ class SearchItem(val key: String, val content: String = key) : AbstractItem Unit)? = null var closeListener: ((searchView: SearchView) -> Unit)? = null + /** + * StringRes for a "no results found" item + * If [results] is ever set to an empty list, it will default to + * a list with one item with this string + * + * For simplicity, kau contains [R.string.kau_no_results_found] + * which you may use + */ + 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 + */ + var textObserver: (observable: Observable, searchView: SearchView) -> Unit = { _, _ -> } } + /** + * Contract for adapter items + * Setting results will ensure that the values are sent on the UI thread + */ + var results: List + get() = adapter.adapterItems + set(value) = context.runOnUiThread { + cardTransition() + adapter.setNewList( + if (configs.noResultsFound > 0 && value.isEmpty()) + listOf(SearchItem("", context.string(configs.noResultsFound), null)) + else value) + } + + + fun clearResults() = context.runOnUiThread { adapter.clear() } + val configs = Configs() //views - val shadow: View by bindView(R.id.search_shadow) - val card: CardView by bindView(R.id.search_cardview) - val iconNav: ImageView by bindView(R.id.search_nav) - val editText: AppCompatEditText by bindView(R.id.search_edit_text) - val progress: ProgressBar by bindView(R.id.search_progress) - val iconMic: ImageView by bindView(R.id.search_mic) - val iconClear: ImageView by bindView(R.id.search_clear) - val recycler: RecyclerView by bindView(R.id.search_recycler) + private val shadow: View by bindView(R.id.search_shadow) + private val card: CardView by bindView(R.id.search_cardview) + private val iconNav: ImageView by bindView(R.id.search_nav) + private val editText: AppCompatEditText by bindView(R.id.search_edit_text) + val textEvents: Observable + private val progress: ProgressBar by bindView(R.id.search_progress) + private val iconMic: ImageView by bindView(R.id.search_mic) + private val iconClear: ImageView by bindView(R.id.search_clear) + private val recycler: RecyclerView by bindView(R.id.search_recycler) val adapter = FastItemAdapter() lateinit var parent: ViewGroup val isOpen: Boolean get() = card.isVisible() - //menu view - var menuX: Int = -1 - var menuY: Int = -1 - var menuHalfHeight: Int = -1 + /* + * Ripple start points and search view offset + * These are calculated every time the search view is opened, + * and can be overridden with the open listener if necessary + */ + var menuX: Int = -1 //starting x for circular reveal + var menuY: Int = -1 //reference for cardview's marginTop + var menuHalfHeight: Int = -1 //starting y for circular reveal (relative to the cardview) init { View.inflate(context, R.layout.kau_search_view, this) iconNav.setSearchIcon(configs.navIcon) iconMic.setSearchIcon(configs.micIcon) - iconClear.setSearchIcon(configs.clearIcon) + iconClear.setSearchIcon(configs.clearIcon).setOnClickListener { + editText.text.clear() + } tintForeground(configs.foregroundColor) tintBackground(configs.backgroundColor) with(recycler) { @@ -102,10 +152,21 @@ class SearchView @JvmOverloads constructor( }) adapter = this@SearchView.adapter } + textEvents = RxTextView.textChangeEvents(editText) + .skipInitialValue() + .observeOn(Schedulers.newThread()) + .map { it.text().toString().trim() } + textEvents.filter { it.isBlank() } + .subscribe { clearResults() } } - internal fun ImageView.setSearchIcon(iicon: IIcon?) { + internal fun ImageView.setSearchIcon(iicon: IIcon?): ImageView { setIcon(iicon, sizeDp = 18, color = configs.foregroundColor) + return this + } + + internal fun cardTransition(builder: AutoTransition.() -> Unit = {}) { + card.transitionAuto { duration = configs.transitionDuration; builder() } } fun config(config: Configs.() -> Unit) { @@ -114,6 +175,7 @@ class SearchView @JvmOverloads constructor( fun bind(parent: ViewGroup, menu: Menu, @IdRes id: Int, config: Configs.() -> Unit = {}): SearchView { config(config) + configs.textObserver(textEvents.filter { it.isNotBlank() }, this) this.parent = parent val item = menu.findItem(id) if (item.icon == null) item.icon = GoogleMaterial.Icon.gmd_search.toDrawable(context, 20) @@ -133,9 +195,6 @@ class SearchView @JvmOverloads constructor( card.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener { override fun onPreDraw(): Boolean { view.viewTreeObserver.removeOnPreDrawListener(this) - with(card) { - KL.e("S $width $measuredWidth $height $measuredHeight") - } val topAlignment = menuY - card.height / 2 val params = (card.layoutParams as MarginLayoutParams).apply { topMargin = topAlignment @@ -151,6 +210,8 @@ class SearchView @JvmOverloads constructor( iconMic.drawable.setTint(color) iconClear.drawable.setTint(color) SearchItem.foregroundColor = color + editText.tint(color) + editText.setTextColor(ColorStateList.valueOf(color)) } fun tintBackground(@ColorInt color: Int) { @@ -164,25 +225,36 @@ class SearchView @JvmOverloads constructor( * We therefore use half the menuItem height, which is a close approximation to our intended value * The cardView matches the parent's width, so menuX is correct */ - card.circularReveal(menuX, menuHalfHeight, duration = configs.revealDuration, - onStart = { - configs.openListener?.invoke(this) - if (configs.shouldClearOnOpen) editText.text.clear() - }, - onFinish = { - editText.requestFocus() - shadow.fadeIn() - }) + configs.openListener?.invoke(this) + card.circularReveal(menuX, menuHalfHeight, duration = configs.revealDuration) { + editText.showKeyboard() + cardTransition() + recycler.visible() + shadow.fadeIn() + } } fun revealClose() { if (!isOpen) return - shadow.fadeOut() { - card.circularHide(menuX, menuHalfHeight, duration = configs.revealDuration, - onFinish = { - configs.closeListener?.invoke(this) - }) + editText.hideKeyboard() + shadow.fadeOut(duration = configs.transitionDuration) + cardTransition { + addEndListener { + card.circularHide(menuX, menuHalfHeight, duration = configs.revealDuration, + onFinish = { + configs.closeListener?.invoke(this@SearchView) + if (configs.shouldClearOnClose) editText.text.clear() + recycler.gone() + }) + } } + recycler.gone() +// card.circularHide(menuX, menuHalfHeight, offset = 100, duration = configs.revealDuration, +// onFinish = { +// configs.closeListener?.invoke(this) +// if (configs.shouldClearOnClose) editText.text.clear() +// recycler.gone() +// }) } } diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt index 7a13685..f712337 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt @@ -55,4 +55,6 @@ fun Activity.setMenuIcons(menu: Menu, @ColorInt color: Int = Color.WHITE, vararg } } -fun Activity.hideKeyboard() = currentFocus.hideKeyboard() \ No newline at end of file +fun Activity.hideKeyboard() = currentFocus.hideKeyboard() + +fun Activity.showKeyboard() = currentFocus.showKeyboard() \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt new file mode 100644 index 0000000..5fa9150 --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt @@ -0,0 +1,19 @@ +package ca.allanwang.kau.utils + +import android.support.transition.Transition +import android.support.transition.TransitionSet + +/** + * Created by Allan Wang on 2017-06-24. + */ +class TransitionEndListener(val onEnd: (transition: Transition) -> Unit) : Transition.TransitionListener { + override fun onTransitionEnd(transition: Transition) = onEnd(transition) + override fun onTransitionResume(transition: Transition) {} + override fun onTransitionPause(transition: Transition) {} + override fun onTransitionCancel(transition: Transition) {} + override fun onTransitionStart(transition: Transition) {} +} + +fun TransitionSet.addEndListener(onEnd: (transition: Transition) -> Unit) { + addListener(TransitionEndListener { onEnd }) +} \ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt index 4ec7a4e..20ffb4b 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt @@ -1,15 +1,15 @@ package ca.allanwang.kau.utils -import android.app.Activity import android.content.Context import android.graphics.Color import android.support.annotation.ColorInt import android.support.annotation.StringRes import android.support.design.widget.Snackbar +import android.support.transition.AutoTransition +import android.support.transition.TransitionManager import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager -import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import com.mikepenz.iconics.IconicsDrawable @@ -61,3 +61,13 @@ fun View.hideKeyboard() { (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(windowToken, 0) } +fun View.showKeyboard() { + requestFocus() + (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) +} + +fun ViewGroup.transitionAuto(builder: AutoTransition.() -> Unit = {}) { + val transition = AutoTransition() + transition.builder() + TransitionManager.beginDelayedTransition(this, transition) +} diff --git a/library/src/main/res/layout/kau_search_view.xml b/library/src/main/res/layout/kau_search_view.xml index 72cfd02..5626c4b 100644 --- a/library/src/main/res/layout/kau_search_view.xml +++ b/library/src/main/res/layout/kau_search_view.xml @@ -13,7 +13,9 @@ + android:layout_height="wrap_content" + android:paddingEnd="2dp" + android:paddingStart="2dp"> + \ No newline at end of file diff --git a/library/src/main/res/values/strings_commons.xml b/library/src/main/res/values/strings_commons.xml index b37c1ea..c678ba1 100644 --- a/library/src/main/res/values/strings_commons.xml +++ b/library/src/main/res/values/strings_commons.xml @@ -50,4 +50,5 @@ Most resources are verbatim and x represents a formatted item Warning Yes Search + No Results Found -- cgit v1.2.3