diff options
Diffstat (limited to 'searchbar')
-rw-r--r-- | searchbar/.gitignore | 1 | ||||
-rw-r--r-- | searchbar/build.gradle | 60 | ||||
-rw-r--r-- | searchbar/progress-proguard.txt | 1 | ||||
-rw-r--r-- | searchbar/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java | 26 | ||||
-rw-r--r-- | searchbar/src/main/AndroidManifest.xml | 1 | ||||
-rw-r--r-- | searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt | 80 | ||||
-rw-r--r-- | searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt | 412 | ||||
-rw-r--r-- | searchbar/src/main/res/layout/kau_search_iitem.xml | 61 | ||||
-rw-r--r-- | searchbar/src/main/res/layout/kau_search_view.xml | 113 | ||||
-rw-r--r-- | searchbar/src/main/res/values/dimens.xml | 27 | ||||
-rw-r--r-- | searchbar/src/main/res/values/ids.xml | 16 | ||||
-rw-r--r-- | searchbar/src/test/java/ca/allanwang/kau/ExampleUnitTest.java | 17 |
12 files changed, 815 insertions, 0 deletions
diff --git a/searchbar/.gitignore b/searchbar/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/searchbar/.gitignore @@ -0,0 +1 @@ +/build diff --git a/searchbar/build.gradle b/searchbar/build.gradle new file mode 100644 index 0000000..80394ea --- /dev/null +++ b/searchbar/build.gradle @@ -0,0 +1,60 @@ +plugins { + id 'com.gladed.androidgitversion' version '0.3.4' +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'com.github.dcendents.android-maven' + +group = project.APP_GROUP + +android { + compileSdkVersion Integer.parseInt(project.TARGET_SDK) + buildToolsVersion project.BUILD_TOOLS + + androidGitVersion { + codeFormat = 'MMNNPPBB' + prefix 'v' + } + + defaultConfig { + minSdkVersion Integer.parseInt(project.MIN_SDK) + targetSdkVersion Integer.parseInt(project.TARGET_SDK) + versionCode androidGitVersion.code() + versionName androidGitVersion.name() + consumerProguardFiles 'progress-proguard.txt' + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError false + checkReleaseBuilds false + } + resourcePrefix "kau_search_" + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + test.java.srcDirs += 'src/test/kotlin' + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { + exclude group: 'com.android.support', module: 'support-annotations' + }) + testCompile 'junit:junit:4.12' + + compile project(':core-ui') + compile project(':adapter') + + compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + +} + +apply from: '../artifacts.gradle' diff --git a/searchbar/progress-proguard.txt b/searchbar/progress-proguard.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/searchbar/progress-proguard.txt @@ -0,0 +1 @@ + diff --git a/searchbar/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java b/searchbar/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java new file mode 100644 index 0000000..7b079b2 --- /dev/null +++ b/searchbar/src/androidTest/java/ca/allanwang/kau/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package ca.allanwang.kau; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("ca.allanwang.kau.test", appContext.getPackageName()); + } +} diff --git a/searchbar/src/main/AndroidManifest.xml b/searchbar/src/main/AndroidManifest.xml new file mode 100644 index 0000000..f2ec3e7 --- /dev/null +++ b/searchbar/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="ca.allanwang.kau.searchview" /> diff --git a/searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt b/searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt new file mode 100644 index 0000000..f1e49e0 --- /dev/null +++ b/searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt @@ -0,0 +1,80 @@ +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.R +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/searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt new file mode 100644 index 0000000..83ccb45 --- /dev/null +++ b/searchbar/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt @@ -0,0 +1,412 @@ +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.R +import ca.allanwang.kau.animators.NoAnimator +import ca.allanwang.kau.kotlin.nonReadable +import ca.allanwang.kau.searchview.SearchView.Configs +import ca.allanwang.kau.utils.* +import ca.allanwang.kau.ui.views.BoundedCardView +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/searchbar/src/main/res/layout/kau_search_iitem.xml b/searchbar/src/main/res/layout/kau_search_iitem.xml new file mode 100644 index 0000000..16df945 --- /dev/null +++ b/searchbar/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/searchbar/src/main/res/layout/kau_search_view.xml b/searchbar/src/main/res/layout/kau_search_view.xml new file mode 100644 index 0000000..c975dad --- /dev/null +++ b/searchbar/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/searchbar/src/main/res/values/dimens.xml b/searchbar/src/main/res/values/dimens.xml new file mode 100644 index 0000000..acf79cb --- /dev/null +++ b/searchbar/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/searchbar/src/main/res/values/ids.xml b/searchbar/src/main/res/values/ids.xml new file mode 100644 index 0000000..b77ad4e --- /dev/null +++ b/searchbar/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 diff --git a/searchbar/src/test/java/ca/allanwang/kau/ExampleUnitTest.java b/searchbar/src/test/java/ca/allanwang/kau/ExampleUnitTest.java new file mode 100644 index 0000000..a29b447 --- /dev/null +++ b/searchbar/src/test/java/ca/allanwang/kau/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package ca.allanwang.kau; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +}
\ No newline at end of file |