diff options
Diffstat (limited to 'library/src/main/kotlin/ca/allanwang')
5 files changed, 147 insertions, 44 deletions
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<SearchItem, SearchItem.ViewHolder>() { +class SearchItem(val key: String, val content: String = key, val iicon: IIcon? = GoogleMaterial.Icon.gmd_search +) : AbstractItem<SearchItem, SearchItem.ViewHolder>() { 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<Sear super.bindView(holder, payloads) if (foregroundColor != null) { holder.text.setTextColor(foregroundColor!!) - holder.icon.drawable.setTint(foregroundColor!!) + holder.icon.setIcon(iicon, sizeDp = 18, color = foregroundColor!!) } holder.text.text = content } @@ -40,14 +43,11 @@ class SearchItem(val key: String, val content: String = key) : AbstractItem<Sear override fun unbindView(holder: ViewHolder) { super.unbindView(holder) holder.text.text = null + holder.icon.setImageDrawable(null) } class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { val icon: ImageView by bindView(R.id.search_icon) val text: TextView by bindView(R.id.search_text) - - init { - - } } }
\ No newline at end of file diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt index 4449fe5..6db75dc 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt @@ -1,8 +1,10 @@ package ca.allanwang.kau.searchview import android.content.Context +import android.content.res.ColorStateList import android.support.annotation.ColorInt import android.support.annotation.IdRes +import android.support.transition.AutoTransition import android.support.v7.widget.AppCompatEditText import android.support.v7.widget.CardView import android.support.v7.widget.LinearLayoutManager @@ -13,15 +15,23 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.ProgressBar import ca.allanwang.kau.R -import ca.allanwang.kau.logging.KL 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 + * + * Huge thanks to @lapism for his base + * https://github.com/lapism/SearchView */ class SearchView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 @@ -60,36 +70,76 @@ class SearchView @JvmOverloads constructor( if (value == null) iconClear.gone() } var revealDuration: Long = 300L - var shouldClearOnOpen: Boolean = true + var transitionDuration: Long = 100L + var shouldClearOnClose: Boolean = true var openListener: ((searchView: SearchView) -> 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<String>, searchView: SearchView) -> Unit = { _, _ -> } } + /** + * Contract for adapter items + * Setting results will ensure that the values are sent on the UI thread + */ + var results: List<SearchItem> + 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<String> + 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<SearchItem>() 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) +} |