diff options
8 files changed, 241 insertions, 61 deletions
@@ -38,6 +38,7 @@ dependencies { * [KPrefs](#kprefs) * [KPref Items](#kpref-items) * [Changelog XML](#changelog) +* [Search View](#search-view) * [Ripple Canvas](#ripple-canvas) * [Timber Logger](#timber-logger) * [Extensions](#extensions) @@ -169,6 +170,21 @@ There is an optional `customize` argument to modify the builder before showing t As mentioned, blank items will be ignored, so feel free to create a bunch of empty lines to facilitate updating the items in the future. +<a name="search-view"></a> +## Search View + +Kau contains a fully functional SearchView that can be added programmatically with one line. +It contains the `bindSearchView` extension functions from both an activity or viewgroup. + +<img src="https://github.com/AllanWang/Storage-Hub/blob/master/kau/kau_search_view.gif"> + +The search view 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. + <a name="ripple-canvas"></a> ## Ripple Canvas diff --git a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchCard.kt b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchCard.kt deleted file mode 100644 index 74f72d0..0000000 --- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchCard.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ca.allanwang.kau.searchview - -import android.content.Context -import android.graphics.Rect -import android.support.v7.widget.CardView -import android.util.AttributeSet -import android.view.ViewGroup -import ca.allanwang.kau.utils.parentViewGroup - -/** - * Created by Allan Wang on 2017-06-26. - * - * CardView with a limited height - * Leaves space for users to tap to exit and ensures that all search items are visible - */ -class SearchCard @JvmOverloads constructor( - context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : CardView(context, attrs, defStyleAttr) { - - val parentVisibleHeight: Int - get() { - val r = Rect() - parentViewGroup.getWindowVisibleDisplayFrame(r) - return r.height() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val trueHeightMeasureSpec = MeasureSpec.makeMeasureSpec((parentVisibleHeight * 0.9f).toInt(), MeasureSpec.AT_MOST) - super.onMeasure(widthMeasureSpec, trueHeightMeasureSpec) - } - -}
\ No newline at end of file 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 3882a06..a672e8a 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt @@ -1,11 +1,14 @@ 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.LinearLayout import android.widget.TextView import ca.allanwang.kau.R import ca.allanwang.kau.utils.* @@ -32,6 +35,18 @@ class SearchItem(val key: String, 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 getLayoutRes(): Int = R.layout.kau_search_item override fun getType(): Int = R.id.kau_item_search @@ -47,7 +62,7 @@ class SearchItem(val key: String, else holder.icon.setIcon(iicon, sizeDp = 18, color = foregroundColor) holder.container.setRippleBackground(foregroundColor, backgroundColor) - holder.title.text = content + holder.title.text = styledContent ?: content if (description?.isNotBlank() ?: false) holder.desc.visible().text = description } 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 ba8243d..a0330c5 100644 --- a/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt +++ b/library/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt @@ -19,7 +19,9 @@ import android.widget.ImageView import android.widget.ProgressBar import ca.allanwang.kau.R import ca.allanwang.kau.kotlin.nonReadable +import ca.allanwang.kau.searchview.SearchView.Configs import ca.allanwang.kau.utils.* +import ca.allanwang.kau.views.KauBoundedCardView import com.jakewharton.rxbinding2.widget.RxTextView import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter import com.mikepenz.google_material_typeface_library.GoogleMaterial @@ -33,6 +35,9 @@ 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 @@ -41,7 +46,16 @@ 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) { @@ -49,6 +63,9 @@ class SearchView @JvmOverloads constructor( SearchItem.foregroundColor = value tintForeground(value) } + /** + * Namely the background for the card and recycler view + */ var backgroundColor: Int get() = SearchItem.backgroundColor set(value) { @@ -56,49 +73,78 @@ class SearchView @JvmOverloads constructor( 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 - * Make sure you add a click listener to [iconExtra] if you plan on using this + * 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 extraIcon: IIcon? = null - set(value) { - field = value - iconExtra.setSearchIcon(value) - if (value == null) iconClear.gone() - } 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 adapter 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 foreground color + * 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() @@ -130,8 +176,11 @@ class SearchView @JvmOverloads constructor( * 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 } /** @@ -141,11 +190,12 @@ class SearchView @JvmOverloads constructor( 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( - if (configs.noResultsFound > 0 && value.isEmpty()) - listOf(SearchItem("", context.string(configs.noResultsFound), iicon = null)) - else value) + adapter.setNewList(list) } /** @@ -157,7 +207,7 @@ class SearchView @JvmOverloads constructor( val configs = Configs() //views private val shadow: View by bindView(R.id.search_shadow) - private val card: SearchCard by bindView(R.id.search_cardview) + private val card: KauBoundedCardView 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> @@ -229,7 +279,12 @@ class SearchView @JvmOverloads constructor( configs.config() } - fun bind(activity: Activity, menu: Menu, @IdRes id: Int, @ColorInt menuIconColor: Int = Color.WHITE, config: Configs.() -> Unit = {}): SearchView { + /** + * 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) @@ -334,11 +389,20 @@ class SearchView @JvmOverloads constructor( @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 { - val searchView = SearchView(this) +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) + + +@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) - findViewById<ViewGroup>(android.R.id.content).addView(searchView) - searchView.bind(this, menu, id, menuIconColor, config) + addView(searchView) + searchView.bind(menu, id, menuIconColor, config) return searchView } + diff --git a/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt b/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt new file mode 100644 index 0000000..a07a118 --- /dev/null +++ b/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt @@ -0,0 +1,56 @@ +package ca.allanwang.kau.views + +import android.content.Context +import android.graphics.Rect +import android.support.v7.widget.CardView +import android.util.AttributeSet +import ca.allanwang.kau.R +import ca.allanwang.kau.utils.parentViewGroup + + +/** + * Created by Allan Wang on 2017-06-26. + * + * CardView with a limited height + * This view should be used with wrap_content as its height + * Defaults to at most the parent's visible height + */ +class KauBoundedCardView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : CardView(context, attrs, defStyleAttr) { + + val parentVisibleHeight: Int + get() { + val r = Rect() + parentViewGroup.getWindowVisibleDisplayFrame(r) + return r.height() + } + + /** + * Maximum height possible, defined in dp (will be converted to px) + * Defaults to parent's visible height + */ + var maxHeight: Int = -1 + /** + * Percentage of resulting max height to fill + * Negative value = fill all of maxHeight + */ + var maxHeightPercent: Float = -1.0f + + init { + if (attrs != null) { + val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.KauBoundedCardView) + maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.KauBoundedCardView_kau_maxHeight, -1) + maxHeightPercent = styledAttrs.getFloat(R.styleable.KauBoundedCardView_kau_maxHeightPercent, -1.0f) + styledAttrs.recycle() + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + var maxMeasureHeight = if (maxHeight > 0) maxHeight else parentVisibleHeight + if (maxHeightPercent > 0f) maxMeasureHeight = (maxMeasureHeight * maxHeightPercent).toInt() + val trueHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMeasureHeight, MeasureSpec.AT_MOST) + super.onMeasure(widthMeasureSpec, trueHeightMeasureSpec) + } + +}
\ No newline at end of file diff --git a/library/src/main/res/layout/kau_search_view.xml b/library/src/main/res/layout/kau_search_view.xml index 4a46e90..4083f44 100644 --- a/library/src/main/res/layout/kau_search_view.xml +++ b/library/src/main/res/layout/kau_search_view.xml @@ -11,13 +11,14 @@ android:background="@color/kau_search_full_shadow" android:visibility="gone" /> - <ca.allanwang.kau.searchview.SearchCard + <ca.allanwang.kau.views.KauBoundedCardView android:id="@+id/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:cardCornerRadius="@dimen/kau_search_base_corners" + app:kau_maxHeightPercent="0.9"> <LinearLayout android:layout_width="match_parent" @@ -107,6 +108,6 @@ </LinearLayout> - </ca.allanwang.kau.searchview.SearchCard> + </ca.allanwang.kau.views.KauBoundedCardView> </merge>
\ No newline at end of file diff --git a/library/src/main/res/values/attr.xml b/library/src/main/res/values/attr.xml new file mode 100644 index 0000000..894a069 --- /dev/null +++ b/library/src/main/res/values/attr.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <declare-styleable name="KauBoundedCardView"> + <attr name="kau_maxHeight" format="dimension" /> + <attr name="kau_maxHeightPercent" format="float" /> + </declare-styleable> + +</resources>
\ No newline at end of file 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 46f4cd2..4c22c2a 100644 --- a/sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt +++ b/sample/src/main/kotlin/ca/allanwang/kau/sample/MainActivity.kt @@ -7,7 +7,6 @@ import ca.allanwang.kau.email.sendEmail import ca.allanwang.kau.kpref.CoreAttributeContract import ca.allanwang.kau.kpref.KPrefActivity import ca.allanwang.kau.kpref.KPrefAdapterBuilder -import ca.allanwang.kau.logging.KL import ca.allanwang.kau.searchview.SearchItem import ca.allanwang.kau.searchview.SearchView import ca.allanwang.kau.searchview.bindSearchView @@ -22,6 +21,54 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial class MainActivity : KPrefActivity() { lateinit var searchView: SearchView + //some of the most common english words for show + val wordBank: List<String> by lazy { + listOf("the", "name", "of", "very", "to", "through", + "and", "just", "a", "form", "in", "much", "is", "great", "it", "think", "you", "say", + "that", "help", "he", "low", "was", "line", "for", "before", "on", "turn", "are", "cause", + "with", "same", "as", "mean", "I", "differ", "his", "move", "they", "right", "be", "boy", + "at", "old", "one", "too", "have", "does", "this", "tell", "from", "sentence", "or", "set", + "had", "three", "by", "want", "hot", "air", "but", "well", "some", "also", "what", "play", + "there", "small", "we", "end", "can", "put", "out", "home", "other", "read", "were", "hand", + "all", "port", "your", "large", "when", "spell", "up", "add", "use", "even", "word", "land", + "how", "here", "said", "must", "an", "big", "each", "high", "she", "such", "which", "follow", + "do", "act", "their", "why", "time", "ask", "if", "men", "will", "change", "way", "went", + "about", "light", "many", "kind", "then", "off", "them", "need", "would", "house", "write", + "picture", "like", "try", "so", "us", "these", "again", "her", "animal", "long", "point", + "make", "mother", "thing", "world", "see", "near", "him", "build", "two", "self", "has", + "earth", "look", "father", "more", "head", "day", "stand", "could", "own", "go", "page", + "come", "should", "did", "country", "my", "found", "sound", "answer", "no", "school", "most", + "grow", "number", "study", "who", "still", "over", "learn", "know", "plant", "water", "cover", + "than", "food", "call", "sun", "first", "four", "people", "thought", "may", "let", "down", "keep", + "side", "eye", "been", "never", "now", "last", "find", "door", "any", "between", "new", "city", + "work", "tree", "part", "cross", "take", "since", "get", "hard", "place", "start", "made", + "might", "live", "story", "where", "saw", "after", "far", "back", "sea", "little", "draw", + "only", "left", "round", "late", "man", "run", "year", "don't", "came", "while", "show", + "press", "every", "close", "good", "night", "me", "real", "give", "life", "our", "few", "under", + "stopRankWordRankWord", "open", "ten", "seem", "simple", "together", "several", "next", + "vowel", "white", "toward", "children", "war", "begin", "lay", "got", "against", "walk", "pattern", + "example", "slow", "ease", "center", "paper", "love", "often", "person", "always", "money", + "music", "serve", "those", "appear", "both", "road", "mark", "map", "book", "science", "letter", + "rule", "until", "govern", "mile", "pull", "river", "cold", "car", "notice", "feet", "voice", + "care", "fall", "second", "power", "group", "town", "carry", "fine", "took", "certain", "rain", + "fly", "eat", "unit", "room", "lead", "friend", "cry", "began", "dark", "idea", "machine", + "fish", "note", "mountain", "wait", "north", "plan", "once", "figure", "base", "star", "hear", + "box", "horse", "noun", "cut", "field", "sure", "rest", "watch", "correct", "color", "able", + "face", "pound", "wood", "done", "main", "beauty", "enough", "drive", "plain", "stood", "girl", + "contain", "usual", "front", "young", "teach", "ready", "week", "above", "final", "ever", "gave", + "red", "green", "list", "oh", "though", "quick", "feel", "develop", "talk", "sleep", "bird", + "warm", "soon", "free", "body", "minute", "dog", "strong", "family", "special", "direct", "mind", + "pose", "behind", "leave", "clear", "song", "tail", "measure", "produce", "state", "fact", "product", + "street", "black", "inch", "short", "lot", "numeral", "nothing", "class", "course", "wind", "stay", + "question", "wheel", "happen", "full", "complete", "force", "ship", "blue", "area", "object", "half", + "decide", "rock", "surface", "order", "deep", "fire", "moon", "south", "island", "problem", "foot", + "piece", "yet", "told", "busy", "knew", "test", "pass", "record", "farm", "boat", "top", "common", + "whole", "gold", "king", "possible", "size", "plane", "heard", "age", "best", "dry", "hour", "wonder", + "better", "laugh", "true.", "thousand", "during", "ago", "hundred", "ran", "am", "check", "remember", + "game", "step", "shape", "early", "yes", "hold", "hot", "west", "miss", "ground", "brought", "interest", + "heat", "reach", "snow", "fast", "bed", "five", "bring", "sing", "sit", "listen", "perhaps", "six", + "fill", "table", "east", "travel", "weight", "less", "language", "morning", "among") + } override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = { textColor = { KPrefSample.textColor } @@ -134,10 +181,14 @@ class MainActivity : KPrefActivity() { 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 -> - KL.e(text) - searchView.results = if (text.length == 3) emptyList() else Array<String>(text.length, { text }).map { SearchItem(it, description = it) } + val items = wordBank.filter { it.contains(text) }.sorted().map { SearchItem(it) } + searchView.results = items } } noResultsFound = R.string.kau_no_results_found |