aboutsummaryrefslogtreecommitdiff
path: root/searchview/src
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-07-09 11:28:59 -0700
committerGitHub <noreply@github.com>2017-07-09 11:28:59 -0700
commite6ec9c5dd677be298430cb2b694f047c67b03239 (patch)
treef0bc2a630c9335b608ac9076ca868f5a5658c75c /searchview/src
parent0af8b379fc0968c830c8938e1998b5bd3dd7e0aa (diff)
downloadkau-e6ec9c5dd677be298430cb2b694f047c67b03239.tar.gz
kau-e6ec9c5dd677be298430cb2b694f047c67b03239.tar.bz2
kau-e6ec9c5dd677be298430cb2b694f047c67b03239.zip
KAU 2.0 (#1) - Update docs and finish up refractoring
* Update readme * Add docs for remaining submodules * Update searchview * Update links 1/3 * Update links 2/3 * Update links 3/3 * update changelog * Update readme * Test rename to readme * Rename remaining to readme * Link readme to repo
Diffstat (limited to 'searchview/src')
-rw-r--r--searchview/src/main/AndroidManifest.xml1
-rw-r--r--searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt79
-rw-r--r--searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt411
-rw-r--r--searchview/src/main/res/layout/kau_search_iitem.xml61
-rw-r--r--searchview/src/main/res/layout/kau_search_view.xml113
-rw-r--r--searchview/src/main/res/values/dimens.xml27
-rw-r--r--searchview/src/main/res/values/ids.xml16
7 files changed, 708 insertions, 0 deletions
diff --git a/searchview/src/main/AndroidManifest.xml b/searchview/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..f2ec3e7
--- /dev/null
+++ b/searchview/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ca.allanwang.kau.searchview" />
diff --git a/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt
new file mode 100644
index 0000000..75d9b27
--- /dev/null
+++ b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt
@@ -0,0 +1,79 @@
+package ca.allanwang.kau.searchview
+
+import android.graphics.Typeface
+import android.graphics.drawable.Drawable
+import android.support.constraint.ConstraintLayout
+import android.support.v7.widget.RecyclerView
+import android.text.Spannable
+import android.text.SpannableStringBuilder
+import android.text.style.StyleSpan
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.iitems.KauIItem
+import ca.allanwang.kau.utils.*
+import com.mikepenz.google_material_typeface_library.GoogleMaterial
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-23.
+ *
+ * A holder for each individual search item
+ * 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,
+ val description: String? = null,
+ val iicon: IIcon? = GoogleMaterial.Icon.gmd_search,
+ val image: Drawable? = null
+) : KauIItem<SearchItem, SearchItem.ViewHolder>(
+ R.layout.kau_search_iitem,
+ { ViewHolder(it) },
+ R.id.kau_item_search
+) {
+
+ companion object {
+ @JvmStatic var foregroundColor: Int = 0xdd000000.toInt()
+ @JvmStatic var backgroundColor: Int = 0xfffafafa.toInt()
+ }
+
+ var styledContent: SpannableStringBuilder? = null
+
+ /**
+ * Highlight the subText if it is present in the content
+ */
+ fun withHighlights(subText: String) {
+ val index = content.indexOf(subText)
+ if (index == -1) return
+ styledContent = SpannableStringBuilder(content)
+ styledContent!!.setSpan(StyleSpan(Typeface.BOLD), index, index + subText.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
+ super.bindView(holder, payloads)
+ holder.title.setTextColor(foregroundColor)
+ holder.desc.setTextColor(foregroundColor.adjustAlpha(0.6f))
+
+ if (image != null) holder.icon.setImageDrawable(image)
+ else holder.icon.setIcon(iicon, sizeDp = 18, color = foregroundColor)
+
+ holder.container.setRippleBackground(foregroundColor, backgroundColor)
+ holder.title.text = styledContent ?: content
+ if (description?.isNotBlank() ?: false) holder.desc.visible().text = description
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ holder.title.text = null
+ holder.desc.gone().text = null
+ holder.icon.setImageDrawable(null)
+ }
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val icon: ImageView by bindView(R.id.kau_search_icon)
+ val title: TextView by bindView(R.id.kau_search_title)
+ val desc: TextView by bindView(R.id.kau_search_desc)
+ val container: ConstraintLayout by bindView(R.id.kau_search_item_frame)
+ }
+} \ No newline at end of file
diff --git a/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
new file mode 100644
index 0000000..2178806
--- /dev/null
+++ b/searchview/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
@@ -0,0 +1,411 @@
+package ca.allanwang.kau.searchview
+
+import android.app.Activity
+import android.content.Context
+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.v7.widget.AppCompatEditText
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.util.AttributeSet
+import android.view.*
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.ProgressBar
+import ca.allanwang.kau.animators.NoAnimator
+import ca.allanwang.kau.kotlin.nonReadable
+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
+ * 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
+ *
+ * Huge thanks to @lapism for his base
+ * https://github.com/lapism/SearchView
+ */
+class SearchView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr) {
+
+ /**
+ * 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
+ */
+ inner class Configs {
+ /**
+ * In the searchview, 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)
+ }
+ /**
+ * 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)
+ }
+ /**
+ * 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
+ }
+
+ /**
+ * 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
+ */
+ var revealDuration: Long = 300L
+ /**
+ * Duration for the auto transition, which is namely used to resize the recycler view
+ */
+ var transitionDuration: Long = 100L
+ /**
+ * Defines whether the edit text and mainAdapter should clear themselves when the searchView is closed
+ */
+ var shouldClearOnClose: Boolean = false
+ /**
+ * Callback that will be called every time the searchView opens
+ */
+ var openListener: ((searchView: SearchView) -> Unit)? = null
+ /**
+ * Callback that will be called every time the searchView closes
+ */
+ var closeListener: ((searchView: SearchView) -> Unit)? = null
+ /**
+ * Draw a divider between the search bar and the suggestion items
+ * 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
+ }
+ /**
+ * 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)
+ }
+ /**
+ * 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 = { _, _ -> }
+ /**
+ * Click event for suggestion items
+ * This event is only triggered when [key] is not blank (like in [noResultsFound]
+ */
+ var onItemClick: (position: Int, key: String, content: String, searchView: SearchView) -> Unit = { _, _, _, _ -> }
+ /**
+ * Long click event for suggestion items
+ * This event is only triggered when [key] is not blank (like in [noResultsFound]
+ */
+ var onItemLongClick: (position: Int, key: String, content: String, searchView: SearchView) -> Unit = { _, _, _, _ -> }
+ /**
+ * If a [SearchItem]'s title contains the submitted query, make that portion bold
+ * See [SearchItem.withHighlights]
+ */
+ var highlightQueryText: Boolean = true
+ }
+
+ /**
+ * Contract for mainAdapter 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 {
+ val list = if (value.isEmpty() && configs.noResultsFound > 0)
+ listOf(SearchItem("", context.string(configs.noResultsFound), iicon = null))
+ else value
+ if (configs.highlightQueryText && value.isNotEmpty()) list.forEach { it.withHighlights(editText.text.toString()) }
+ cardTransition()
+ adapter.setNewList(list)
+ }
+
+ /**
+ * Empties the list on the UI thread
+ * The noResults item will not be added
+ */
+ internal fun clearResults() = context.runOnUiThread { cardTransition(); adapter.clear() }
+
+ val configs = Configs()
+ //views
+ private val shadow: View by bindView(R.id.kau_search_shadow)
+ 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 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)
+ val adapter = FastItemAdapter<SearchItem>()
+ var menuItem: MenuItem? = null
+ val isOpen: Boolean
+ get() = card.isVisible()
+
+ /*
+ * 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)
+ z = 99f
+ iconNav.setSearchIcon(configs.navIcon).setOnClickListener { revealClose() }
+ iconClear.setSearchIcon(configs.clearIcon).setOnClickListener { editText.text.clear() }
+ tintForeground(configs.foregroundColor)
+ tintBackground(configs.backgroundColor)
+ with(recycler) {
+ isNestedScrollingEnabled = false
+ layoutManager = LinearLayoutManager(context)
+ addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
+ super.onScrollStateChanged(recyclerView, newState)
+ if (newState == RecyclerView.SCROLL_STATE_DRAGGING) hideKeyboard()
+ }
+ })
+ adapter = this@SearchView.adapter
+ itemAnimator = NoAnimator()
+ }
+ with(adapter) {
+ withSelectable(true)
+ withOnClickListener { _, _, item, position ->
+ if (item.key.isNotBlank()) configs.onItemClick(position, item.key, item.content, this@SearchView); true
+ }
+ withOnLongClickListener { _, _, item, position ->
+ 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() }
+ }
+
+ 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) {
+ configs.config()
+ }
+
+ /**
+ * Binds the SearchView to a menu item and handles everything internally
+ * This is assuming that SearchView has already been added to a ViewGroup
+ * If not, see the extension function [bindSearchView]
+ */
+ 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)
+ if (menuItem!!.icon == null) menuItem!!.icon = GoogleMaterial.Icon.gmd_search.toDrawable(context, 18, menuIconColor)
+ card.gone()
+ menuItem!!.setOnMenuItemClickListener { configureCoords(it); revealOpen(); true }
+ shadow.setOnClickListener { revealClose() }
+ return this
+ }
+
+ fun unBind(replacementMenuItemClickListener: MenuItem.OnMenuItemClickListener? = null) {
+ parentViewGroup.removeView(this)
+ menuItem?.setOnMenuItemClickListener(replacementMenuItemClickListener)
+ }
+
+ fun configureCoords(item: MenuItem) {
+ val view = parentViewGroup.findViewById<View>(item.itemId) ?: return
+ val locations = IntArray(2)
+ view.getLocationOnScreen(locations)
+ menuX = (locations[0] + view.width / 2)
+ menuHalfHeight = view.height / 2
+ menuY = (locations[1] + menuHalfHeight)
+ card.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ view.viewTreeObserver.removeOnPreDrawListener(this)
+ val topAlignment = menuY - card.height / 2
+ val params = (card.layoutParams as MarginLayoutParams).apply {
+ topMargin = topAlignment
+ }
+ card.layoutParams = params
+ return false
+ }
+ })
+ }
+
+ /**
+ * Handle a back press event
+ * Returns true if back press is consumed, false otherwise
+ */
+ fun onBackPressed(): Boolean {
+ if (isOpen && menuItem != null) {
+ revealClose()
+ return true
+ }
+ return false
+ }
+
+ /**
+ * Tint foreground attributes
+ * This can be done publicly through [configs], which will also save the color
+ */
+ internal fun tintForeground(@ColorInt color: Int) {
+ iconNav.drawable.setTint(color)
+ iconClear.drawable.setTint(color)
+ divider.setBackgroundColor(color.adjustAlpha(0.1f))
+ editText.tint(color)
+ editText.setTextColor(ColorStateList.valueOf(color))
+ }
+
+ /**
+ * Tint background attributes
+ * This can be done publicly through [configs], which will also save the color
+ */
+ internal fun tintBackground(@ColorInt color: Int) {
+ card.setCardBackgroundColor(color)
+ }
+
+ fun revealOpen() {
+ if (isOpen) return
+ /**
+ * The y component is relative to the cardView, but it hasn't been drawn yet so its own height is 0
+ * 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
+ */
+ configs.openListener?.invoke(this)
+ shadow.fadeIn()
+ editText.showKeyboard()
+ card.circularReveal(menuX, menuHalfHeight, duration = configs.revealDuration) {
+ cardTransition()
+ recycler.visible()
+ }
+ }
+
+ fun revealClose() {
+ if (!isOpen) return
+ 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()
+ editText.hideKeyboard()
+ }
+}
+
+@DslMarker
+annotation class KauSearch
+
+/**
+ * Helper function that binds to an activity's main view
+ */
+@KauSearch
+fun Activity.bindSearchView(menu: Menu, @IdRes id: Int, @ColorInt menuIconColor: Int = Color.WHITE, config: SearchView.Configs.() -> Unit = {}): SearchView
+ = findViewById<ViewGroup>(android.R.id.content).bindSearchView(menu, id, menuIconColor, config)
+
+/**
+ * Bind searchView to a menu item; call this in [Activity.onCreateOptionsMenu]
+ * Be wary that if you may reinflate the menu many times (eg through [Activity.invalidateOptionsMenu]),
+ * it may be worthwhile to hold a reference to the searchview and only bind it if it hasn't been bound before
+ */
+@KauSearch
+fun ViewGroup.bindSearchView(menu: Menu, @IdRes id: Int, @ColorInt menuIconColor: Int = Color.WHITE, config: SearchView.Configs.() -> Unit = {}): SearchView {
+ val searchView = SearchView(context)
+ searchView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
+ addView(searchView)
+ searchView.bind(menu, id, menuIconColor, config)
+ return searchView
+}
+
diff --git a/searchview/src/main/res/layout/kau_search_iitem.xml b/searchview/src/main/res/layout/kau_search_iitem.xml
new file mode 100644
index 0000000..16df945
--- /dev/null
+++ b/searchview/src/main/res/layout/kau_search_iitem.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@id/kau_search_item_frame"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_search_item_height"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/kau_search_icon"
+ android:layout_width="@dimen/kau_search_icon"
+ android:layout_height="0dp"
+ android:contentDescription="@string/kau_search"
+ android:scaleType="centerInside"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@id/kau_search_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="start|center_vertical"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/kau_search_key_line_16"
+ android:paddingStart="@dimen/kau_search_key_line_8"
+ android:textIsSelectable="false"
+ android:textSize="@dimen/kau_search_text_small"
+ app:layout_constraintBottom_toTopOf="@id/kau_search_desc"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_search_icon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.5" />
+
+ <TextView
+ android:id="@id/kau_search_desc"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:layout_weight="1"
+ android:ellipsize="end"
+ android:gravity="start|center_vertical"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/kau_search_key_line_16"
+ android:paddingStart="@dimen/kau_search_key_line_8"
+ android:textIsSelectable="false"
+ android:textSize="@dimen/kau_search_text_micro"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_search_icon"
+ app:layout_constraintTop_toBottomOf="@id/kau_search_title" />
+
+</android.support.constraint.ConstraintLayout> \ No newline at end of file
diff --git a/searchview/src/main/res/layout/kau_search_view.xml b/searchview/src/main/res/layout/kau_search_view.xml
new file mode 100644
index 0000000..c975dad
--- /dev/null
+++ b/searchview/src/main/res/layout/kau_search_view.xml
@@ -0,0 +1,113 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <View
+ android:id="@id/kau_search_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/kau_shadow_overlay"
+ android:visibility="gone" />
+
+ <ca.allanwang.kau.ui.views.BoundedCardView
+ android:id="@id/kau_search_cardview"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/kau_search_base_margin"
+ android:layout_marginStart="@dimen/kau_search_base_margin"
+ app:cardCornerRadius="@dimen/kau_search_base_corners"
+ app:maxHeightPercent="0.9">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_search_height"
+ android:focusable="true"
+ android:focusableInTouchMode="true"
+ android:gravity="start|center_vertical"
+ android:orientation="horizontal">
+
+ <ImageView
+ android:id="@id/kau_search_nav"
+ android:layout_width="@dimen/kau_search_icon"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/kau_search"
+ android:focusable="true"
+ android:scaleType="centerInside" />
+
+ <android.support.v7.widget.AppCompatEditText
+ android:id="@id/kau_search_edit_text"
+ android:layout_width="0dp"
+ android:layout_height="match_parent"
+ android:layout_gravity="start|center_vertical"
+ android:layout_weight="1"
+ android:background="@android:color/transparent"
+ android:ellipsize="end"
+ android:ems="10"
+ android:gravity="start|center_vertical"
+ android:imeOptions="actionSearch|flagNoExtractUi"
+ android:inputType="textNoSuggestions"
+ android:maxLines="1"
+ android:paddingEnd="@dimen/kau_search_key_line_16"
+ android:paddingStart="@dimen/kau_search_key_line_8"
+ android:privateImeOptions="nm"
+ android:textSize="@dimen/kau_search_text_medium"
+ android:windowSoftInputMode="stateAlwaysHidden" />
+
+ <ProgressBar
+ android:id="@id/kau_search_progress"
+ style="?android:attr/indeterminateProgressStyle"
+ android:layout_width="@dimen/kau_search_progress"
+ android:layout_height="match_parent"
+ android:visibility="gone" />
+
+ <ImageView
+ android:id="@id/kau_search_extra"
+ android:layout_width="48dp"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/kau_search"
+ android:focusable="true"
+ android:scaleType="center"
+ android:visibility="gone" />
+
+ <ImageView
+ android:id="@id/kau_search_clear"
+ android:layout_width="48dp"
+ android:layout_height="match_parent"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:clickable="true"
+ android:contentDescription="@string/kau_search"
+ android:focusable="true"
+ android:scaleType="center" />
+
+ </LinearLayout>
+
+ <View
+ android:id="@id/kau_search_divider"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_search_divider"
+ android:background="?android:attr/listDivider" />
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@id/kau_search_recycler"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+
+ android:overScrollMode="never"
+ android:visibility="gone" />
+
+ </LinearLayout>
+
+ </ca.allanwang.kau.ui.views.BoundedCardView>
+
+</merge> \ No newline at end of file
diff --git a/searchview/src/main/res/values/dimens.xml b/searchview/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..acf79cb
--- /dev/null
+++ b/searchview/src/main/res/values/dimens.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <dimen name="kau_search_height">46dp</dimen>
+
+ <dimen name="kau_search_base_margin">4dp</dimen>
+ <dimen name="kau_search_base_corners">2dp</dimen>
+ <dimen name="kau_search_key_line_8">8dp</dimen>
+ <dimen name="kau_search_key_line_16">16dp</dimen>
+ <dimen name="kau_search_divider">1dp</dimen>
+
+ <dimen name="kau_search_item_margin_text">24dp</dimen>
+ <dimen name="kau_search_item_height">56dp</dimen>
+ <dimen name="kau_search_icon">56dp</dimen>
+ <dimen name="kau_search_progress">24dp</dimen>
+ <dimen name="kau_search_text_micro">12sp</dimen>
+ <dimen name="kau_search_text_small">14sp</dimen>
+ <dimen name="kau_search_text_medium">16sp</dimen>
+ <dimen name="kau_search_reveal">24dp</dimen>
+
+ <dimen name="kau_search_filter_margin_top">4dp</dimen>
+ <dimen name="kau_search_filter_margin_start">12dp</dimen>
+
+ <dimen name="kau_search_menu_item_margin">1dp</dimen>
+ <dimen name="kau_search_menu_item_margin_left_right">2dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/searchview/src/main/res/values/ids.xml b/searchview/src/main/res/values/ids.xml
new file mode 100644
index 0000000..b77ad4e
--- /dev/null
+++ b/searchview/src/main/res/values/ids.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="kau_search_cardview" type="id" />
+ <item name="kau_search_clear" type="id" />
+ <item name="kau_search_desc" type="id" />
+ <item name="kau_search_divider" type="id" />
+ <item name="kau_search_edit_text" type="id" />
+ <item name="kau_search_extra" type="id" />
+ <item name="kau_search_icon" type="id" />
+ <item name="kau_search_item_frame" type="id" />
+ <item name="kau_search_nav" type="id" />
+ <item name="kau_search_progress" type="id" />
+ <item name="kau_search_recycler" type="id" />
+ <item name="kau_search_shadow" type="id" />
+ <item name="kau_search_title" type="id" />
+</resources> \ No newline at end of file