aboutsummaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/androidTest/java/ca/allanwang/kprefs/library/ExampleInstrumentedTest.java26
-rw-r--r--core/src/main/AndroidManifest.xml1
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt236
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt85
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt189
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt13
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt45
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java764
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt52
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt63
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt51
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt41
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt23
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt105
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt228
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt349
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt82
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerView.kt309
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt92
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt127
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/iitems/CutoutIItem.kt48
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt49
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt23
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/iitems/LibraryIItem.kt100
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt50
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt51
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kotlin/NonReadablePropertyException.kt12
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt35
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt137
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt120
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt89
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt33
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt73
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt25
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt85
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt126
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt29
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefSubItems.kt43
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefText.kt62
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt6
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt19
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionManager.kt58
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionResult.kt26
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/permissions/Permissions.kt68
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt80
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt412
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt71
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt15
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt153
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt235
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt6
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt165
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/Either.kt32
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt29
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt12
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt20
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt166
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt48
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt22
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt19
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt111
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt126
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/views/BoundedCardView.kt50
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/views/CutoutView.kt183
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt140
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/views/SimpleRippleDrawable.kt19
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt234
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java859
-rw-r--r--core/src/main/kotlin/ca/allanwang/kau/widgets/TextSlider.kt125
-rw-r--r--core/src/main/res/anim/kau_fade_in.xml5
-rw-r--r--core/src/main/res/anim/kau_fade_out.xml5
-rw-r--r--core/src/main/res/anim/kau_slide_in_bottom.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_in_left.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_in_right.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_in_top.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_out_bottom.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_out_left.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_out_right.xml7
-rw-r--r--core/src/main/res/anim/kau_slide_out_right_top.xml8
-rw-r--r--core/src/main/res/anim/kau_slide_out_top.xml7
-rw-r--r--core/src/main/res/drawable/kau_transparent.xml5
-rw-r--r--core/src/main/res/layout/kau_about_section_libraries.xml27
-rw-r--r--core/src/main/res/layout/kau_about_section_main.xml46
-rw-r--r--core/src/main/res/layout/kau_activity_about.xml27
-rw-r--r--core/src/main/res/layout/kau_activity_kpref.xml54
-rw-r--r--core/src/main/res/layout/kau_changelog_content.xml28
-rw-r--r--core/src/main/res/layout/kau_changelog_title.xml14
-rw-r--r--core/src/main/res/layout/kau_iitem_card.xml79
-rw-r--r--core/src/main/res/layout/kau_iitem_cutout.xml9
-rw-r--r--core/src/main/res/layout/kau_iitem_header.xml17
-rw-r--r--core/src/main/res/layout/kau_iitem_library.xml124
-rw-r--r--core/src/main/res/layout/kau_preference.xml97
-rw-r--r--core/src/main/res/layout/kau_preference_checkbox.xml7
-rw-r--r--core/src/main/res/layout/kau_preference_color_preview.xml6
-rw-r--r--core/src/main/res/layout/kau_preference_header.xml10
-rw-r--r--core/src/main/res/layout/kau_preference_text.xml7
-rw-r--r--core/src/main/res/layout/kau_recycler_detached_background.xml27
-rw-r--r--core/src/main/res/layout/kau_recycler_textslider.xml34
-rw-r--r--core/src/main/res/layout/kau_search_iitem.xml61
-rw-r--r--core/src/main/res/layout/kau_search_view.xml113
-rw-r--r--core/src/main/res/transition/kau_about_return_downward.xml49
-rw-r--r--core/src/main/res/transition/kau_about_return_upwards.xml50
-rw-r--r--core/src/main/res/transition/kau_enter_slide_top.xml38
-rw-r--r--core/src/main/res/values/attr.xml41
-rw-r--r--core/src/main/res/values/colors.xml6
-rw-r--r--core/src/main/res/values/dimens.xml37
-rw-r--r--core/src/main/res/values/dimens_search.xml27
-rw-r--r--core/src/main/res/values/ids.xml16
-rw-r--r--core/src/main/res/values/strings.xml15
-rw-r--r--core/src/main/res/values/strings_about.xml27
-rw-r--r--core/src/main/res/values/strings_commons.xml57
-rw-r--r--core/src/main/res/values/styles.xml21
-rw-r--r--core/src/main/res/values/styles_animations.xml30
-rw-r--r--core/src/main/res/xml/kau_changelog.xml14
-rw-r--r--core/src/test/kotlin/ca/allanwang/kprefs/library/UtilsTest.kt25
115 files changed, 8899 insertions, 0 deletions
diff --git a/core/src/androidTest/java/ca/allanwang/kprefs/library/ExampleInstrumentedTest.java b/core/src/androidTest/java/ca/allanwang/kprefs/library/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..8a80585
--- /dev/null
+++ b/core/src/androidTest/java/ca/allanwang/kprefs/library/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package ca.allanwang.kprefs.library;
+
+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.kprefs.library.test", appContext.getPackageName());
+ }
+}
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4461514
--- /dev/null
+++ b/core/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ca.allanwang.kau" />
diff --git a/core/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt b/core/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
new file mode 100644
index 0000000..32e8745
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
@@ -0,0 +1,236 @@
+package ca.allanwang.kau.about
+
+import android.graphics.drawable.Drawable
+import android.os.Bundle
+import android.support.v4.view.PagerAdapter
+import android.support.v4.view.ViewPager
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.RecyclerView
+import android.transition.TransitionInflater
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.FastItemThemedAdapter
+import ca.allanwang.kau.adapters.ThemableIItemColors
+import ca.allanwang.kau.adapters.ThemableIItemColorsDelegate
+import ca.allanwang.kau.animators.FadeScaleAnimator
+import ca.allanwang.kau.iitems.CutoutIItem
+import ca.allanwang.kau.iitems.HeaderIItem
+import ca.allanwang.kau.iitems.LibraryIItem
+import ca.allanwang.kau.utils.*
+import ca.allanwang.kau.widgets.ElasticDragDismissFrameLayout
+import ca.allanwang.kau.widgets.InkPageIndicator
+import com.mikepenz.aboutlibraries.Libs
+import com.mikepenz.aboutlibraries.entity.Library
+import com.mikepenz.fastadapter.IItem
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
+import java.security.InvalidParameterException
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Floating About Activity Panel for your app
+ * This contains all the necessary layouts, and can be extended and configured using the [configBuilder]
+ * The [rClass] is necessary to generate the list of libraries used in your app, and should point to your app's
+ * R.string::class.java
+ * Note that for the auto detection to work, the R fields must be excluded from Proguard
+ * Manual lib listings and other extra modifications can be done so by overriding the open functions
+ */
+abstract class AboutActivityBase(val rClass: Class<*>, val configBuilder: Configs.() -> Unit = {}) : AppCompatActivity(), ViewPager.OnPageChangeListener {
+
+ val draggableFrame: ElasticDragDismissFrameLayout by bindView(R.id.about_draggable_frame)
+ val pager: ViewPager by bindView(R.id.about_pager)
+ val indicator: InkPageIndicator by bindView(R.id.about_indicator)
+ /**
+ * Holds some common configurations that may be added directly from the constructor
+ * Applied lazily since it needs the context to fetch resources
+ */
+ val configs: Configs by lazy { Configs().apply { configBuilder() } }
+ /**
+ * Number of pages in the adapter
+ * Defaults to just the main view and lib view
+ */
+ open val pageCount: Int = 2
+ /**
+ * Page position for the libs
+ * This is generated automatically if [inflateLibPage] is called
+ */
+ private var libPage: Int = -2
+ /**
+ * Holds that status of each page
+ * 0 means nothing has happened
+ * 1 means this page has been in view at least once
+ * The rest is up to you
+ */
+ lateinit var pageStatus: IntArray
+ /**
+ * Holds the lib items once they are fetched asynchronously
+ */
+ var libItems: List<LibraryIItem>? = null
+ /**
+ * Holds the adapter for the library page; this is generated later because it uses the config colors
+ */
+ lateinit var libAdapter: FastItemThemedAdapter<IItem<*, *>>
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.kau_activity_about)
+ pageStatus = IntArray(pageCount)
+ libAdapter = FastItemThemedAdapter(configs)
+ LibraryIItem.bindClickEvents(libAdapter)
+ if (configs.textColor != null) indicator.setColour(configs.textColor!!)
+ with(pager) {
+ adapter = AboutPagerAdapter()
+ pageMargin = dimenPixelSize(R.dimen.kau_spacing_normal)
+ addOnPageChangeListener(this@AboutActivityBase)
+ }
+ indicator.setViewPager(pager)
+ draggableFrame.addListener(object : ElasticDragDismissFrameLayout.SystemChromeFader(this) {
+ override fun onDragDismissed() {
+ // if we drag dismiss downward then the default reversal of the enter
+ // transition would slide content upward which looks weird. So reverse it.
+ if (draggableFrame.translationY > 0) {
+ window.returnTransition = TransitionInflater.from(this@AboutActivityBase)
+ .inflateTransition(configs.transitionExitReversed)
+ }
+ finishAfterTransition()
+ }
+ })
+ }
+
+ inner class Configs : ThemableIItemColors by ThemableIItemColorsDelegate() {
+ var cutoutTextRes: Int = -1
+ var cutoutText: String? = null
+ var cutoutDrawableRes: Int = -1
+ var cutoutDrawable: Drawable? = null
+ var cutoutForeground: Int? = null
+ var libPageTitleRes: Int = -1
+ var libPageTitle: String? = string(R.string.kau_about_libraries_intro) //This is in the string by default since it's lower priority
+ /**
+ * Transition to be called if the view is dragged down
+ */
+ var transitionExitReversed: Int = R.transition.kau_about_return_downward
+ }
+
+ /**
+ * Method to fetch the library list
+ * This is fetched asynchronously and you may override it to customize the list
+ */
+ open fun getLibraries(libs: Libs): List<Library> = libs.prepareLibraries(this, null, null, true, true)
+
+ /**
+ * Gets the view associated with the given page position
+ * Keep in mind that when inflating, do NOT add the view to the viewgroup
+ * Use layoutInflater.inflate(id, parent, false)
+ */
+ open fun getPage(position: Int, layoutInflater: LayoutInflater, parent: ViewGroup): View {
+ return when (position) {
+ 0 -> inflateMainPage(layoutInflater, parent, position)
+ pageCount - 1 -> inflateLibPage(layoutInflater, parent, position)
+ else -> throw InvalidParameterException()
+ }
+ }
+
+ /**
+ * Create the main view with the cutout
+ */
+ open fun inflateMainPage(layoutInflater: LayoutInflater, parent: ViewGroup, position: Int): View {
+ val fastAdapter = FastItemThemedAdapter<IItem<*, *>>(configs)
+ val recycler = fullLinearRecycler(fastAdapter)
+ fastAdapter.add(CutoutIItem {
+ with(configs) {
+ text = string(cutoutTextRes, cutoutText)
+ drawable = drawable(cutoutDrawableRes, cutoutDrawable)
+ if (configs.cutoutForeground != null) foregroundColor = configs.cutoutForeground!!
+ }
+ }.apply {
+ themeEnabled = configs.cutoutForeground == null
+ })
+ postInflateMainPage(fastAdapter)
+ return recycler
+ }
+
+ /**
+ * Open hook called just before the main page view is returned
+ * Feel free to add your own items to the adapter in here
+ */
+ open fun postInflateMainPage(adapter: FastItemThemedAdapter<IItem<*, *>>) {
+
+ }
+
+ /**
+ * Create the lib view with the list of libraries
+ */
+ open fun inflateLibPage(layoutInflater: LayoutInflater, parent: ViewGroup, position: Int): View {
+ libPage = position
+ val v = layoutInflater.inflate(R.layout.kau_recycler_detached_background, parent, false)
+ val recycler = v.findViewById<RecyclerView>(R.id.kau_recycler_detached)
+ recycler.adapter = libAdapter
+ recycler.itemAnimator = FadeScaleAnimator(itemDelayFactor = 0.2f).apply { addDuration = 300; interpolator = AnimHolder.decelerateInterpolator(this@AboutActivityBase) }
+ val background = v.findViewById<View>(R.id.kau_recycler_detached_background)
+ if (configs.backgroundColor != null) background.setBackgroundColor(configs.backgroundColor!!.colorToForeground())
+ doAsync {
+ libItems = getLibraries(Libs(this@AboutActivityBase, Libs.toStringArray(rClass.fields))).map { LibraryIItem(it) }
+ if (libPage >= 0 && pageStatus[libPage] == 1)
+ uiThread { addLibItems() }
+ }
+ return v
+ }
+
+ inner class AboutPagerAdapter : PagerAdapter() {
+
+ private val layoutInflater: LayoutInflater = LayoutInflater.from(this@AboutActivityBase)
+ private val views = Array<View?>(pageCount) { null }
+
+ override fun instantiateItem(collection: ViewGroup, position: Int): Any {
+ val layout = getPage(position, collection)
+ collection.addView(layout)
+ return layout
+ }
+
+ override fun destroyItem(collection: ViewGroup, position: Int, view: Any) {
+ collection.removeView(view as View)
+ views[position] = null
+ }
+
+ override fun getCount(): Int = pageCount
+
+ override fun isViewFromObject(view: View, `object`: Any): Boolean = view === `object`
+
+ /**
+ * Only get page if view does not exist
+ */
+ private fun getPage(position: Int, parent: ViewGroup): View {
+ if (views[position] == null) views[position] = getPage(position, layoutInflater, parent)
+ return views[position]!!
+ }
+ }
+
+ override fun onPageScrollStateChanged(state: Int) {}
+
+ override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
+
+ override fun onPageSelected(position: Int) {
+ if (pageStatus[position] == 0) pageStatus[position] = 1 // mark as seen if previously null
+ if (position == libPage && libItems != null && pageStatus[position] == 1) {
+ pageStatus[position] = 2 //add libs and mark as such
+ postDelayed(300) { addLibItems() } //delay so that the animations occur once the page is fully switched
+ }
+ }
+
+ /**
+ * Function that is called when the view is ready to add the lib items
+ * Feel free to add your own items here
+ */
+ open fun addLibItems() {
+ libAdapter.add(HeaderIItem(text = configs.libPageTitle, textRes = configs.libPageTitleRes))
+ .add(libItems)
+ }
+
+ override fun onDestroy() {
+ AnimHolder.decelerateInterpolator.invalidate() //clear the reference to the interpolators we've used
+ super.onDestroy()
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt b/core/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
new file mode 100644
index 0000000..e1c5c18
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/adapters/ChainedAdapters.kt
@@ -0,0 +1,85 @@
+package ca.allanwang.kau.adapters
+
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.adapters.HeaderAdapter
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import org.jetbrains.anko.collections.forEachReversedWithIndex
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Once bounded to a [RecyclerView], this will
+ * - Chain together a list of [HeaderAdapter]s, backed by a generic [FastItemAdapter]
+ * - Add a [LinearLayoutManager] to the recycler
+ * - Add a listener for when a new adapter segment is being used
+ */
+class ChainedAdapters<T>(vararg items: Pair<T, SectionAdapter<*>>) {
+ private val chain: MutableList<Pair<T, SectionAdapter<*>>> = mutableListOf(*items)
+ val baseAdapter: FastItemAdapter<IItem<*, *>> = FastItemAdapter()
+ private val indexStack = Stack<Int>()
+ var recycler: RecyclerView? = null
+ val firstVisibleItemPosition: Int
+ get() = (recycler?.layoutManager as LinearLayoutManager?)?.findFirstVisibleItemPosition() ?: throw IllegalArgumentException("No recyclerview was bounded to the chain adapters")
+
+ fun add(vararg items: Pair<T, SectionAdapter<*>>) = add(items.toList())
+
+ fun add(items: Collection<Pair<T, SectionAdapter<*>>>): ChainedAdapters<T> {
+ if (recycler != null) throw IllegalAccessException("Chain adapter is already bounded to a recycler; cannot add directly.")
+ items.map { it.second }.forEachIndexed { index, sectionAdapter -> sectionAdapter.sectionOrder = chain.size + 1 + index }
+ chain.addAll(items)
+ return this
+ }
+
+ operator fun get(index: Int) = chain[index]
+
+ /**
+ * Attaches the chain to a recycler
+ * After this stage, any modifications to the adapters must be done through external references
+ * You may still get the generic header adapters through the get operator
+ * Binding the recycler also involves supplying a callback, which returns
+ * the item (T) associated with the adapter,
+ * the index (Int) of the current adapter
+ * and the dy (Int) as given by the scroll listener
+ */
+ fun bindRecyclerView(recyclerView: RecyclerView, onAdapterSectionChanged: (item: T, index: Int, dy: Int) -> Unit) {
+ if (recycler != null) throw IllegalStateException("Chain adapter is already bounded")
+ if (chain.isEmpty()) throw IllegalArgumentException("No adapters have been added to the adapters list")
+ //wrap adapters
+ chain.map { it.second }.forEachReversedWithIndex { i, headerAdapter ->
+ if (i == chain.size - 1) headerAdapter.wrap(baseAdapter)
+ else headerAdapter.wrap(chain[i + 1].second)
+ }
+ recycler = recyclerView
+ indexStack.push(0)
+ with(recyclerView) {
+ layoutManager = LinearLayoutManager(context)
+ adapter = chain.first().second
+ addOnScrollListener(object : RecyclerView.OnScrollListener() {
+ override fun onScrolled(rv: RecyclerView, dx: Int, dy: Int) {
+ super.onScrolled(rv, dx, dy)
+ val topPosition = firstVisibleItemPosition
+ val currentAdapterIndex = indexStack.peek()
+ if (dy > 0) {
+ //look ahead from current adapter
+ val nextAdapterIndex = (currentAdapterIndex until chain.size).asSequence()
+ .firstOrNull {
+ val adapter = chain[it].second
+ adapter.adapterItemCount > 0 && adapter.getGlobalPosition(adapter.adapterItemCount - 1) >= topPosition
+ } ?: currentAdapterIndex
+ if (nextAdapterIndex == currentAdapterIndex) return
+ indexStack.push(nextAdapterIndex)
+ onAdapterSectionChanged(chain[indexStack.peek()].first, indexStack.peek(), dy)
+ } else if (currentAdapterIndex == 0) {
+ return //All adapters may be empty; in this case, if we are already at the beginning, don't bother checking
+ } else if (chain[currentAdapterIndex].second.getGlobalPosition(0) > topPosition) {
+ indexStack.pop()
+ onAdapterSectionChanged(chain[indexStack.peek()].first, indexStack.peek(), dy)
+ }
+ }
+ })
+ }
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt b/core/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt
new file mode 100644
index 0000000..66fec4b
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/adapters/FastItemThemedAdapter.kt
@@ -0,0 +1,189 @@
+package ca.allanwang.kau.adapters
+
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.utils.adjustAlpha
+import ca.allanwang.kau.views.createSimpleRippleDrawable
+import com.mikepenz.fastadapter.IExpandable
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.ISubItem
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+
+/**
+ * Created by Allan Wang on 2017-06-29.
+ *
+ * Adapter with a set of colors that will be added to all subsequent items
+ * Changing a color while the adapter is not empty will reload all items
+ *
+ * This adapter overrides every method where an item is added
+ * If that item extends [ThemableIItem], then the colors will be set
+ */
+class FastItemThemedAdapter<Item : IItem<*, *>>(
+ textColor: Int? = null,
+ backgroundColor: Int? = null,
+ accentColor: Int? = null
+) : FastItemAdapter<Item>() {
+ constructor(colors: ThemableIItemColors) : this(colors.textColor, colors.backgroundColor, colors.accentColor)
+
+ var textColor: Int? = textColor
+ set(value) {
+ if (field == value) return
+ field = value
+ themeChanged()
+ }
+ var backgroundColor: Int? = backgroundColor
+ set(value) {
+ if (field == value) return
+ field = value
+ themeChanged()
+ }
+ var accentColor: Int? = accentColor
+ set(value) {
+ if (field == value) return
+ field = value
+ themeChanged()
+ }
+
+ fun setColors(colors: ThemableIItemColors) {
+ this.textColor = colors.textColor
+ this.backgroundColor = colors.backgroundColor
+ this.accentColor = colors.accentColor
+ }
+
+ fun themeChanged() {
+ if (adapterItemCount == 0) return
+ injectTheme(adapterItems)
+ notifyAdapterDataSetChanged()
+ }
+
+ override fun add(position: Int, items: MutableList<Item>): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.add(position, items)
+ }
+
+ override fun add(position: Int, item: Item): FastItemAdapter<Item> {
+ injectTheme(item)
+ return super.add(position, item)
+ }
+
+ override fun add(item: Item): FastItemAdapter<Item> {
+ injectTheme(item)
+ return super.add(item)
+ }
+
+ override fun add(items: MutableList<Item>): FastItemAdapter<Item> {
+ injectTheme(items)
+ injectTheme(items)
+ return super.add(items)
+ }
+
+ override fun set(items: MutableList<Item>?): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.set(items)
+ }
+
+ override fun set(position: Int, item: Item): FastItemAdapter<Item> {
+ injectTheme(item)
+ return super.set(position, item)
+ }
+
+ override fun setNewList(items: MutableList<Item>?, retainFilter: Boolean): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.setNewList(items, retainFilter)
+ }
+
+ override fun setNewList(items: MutableList<Item>?): FastItemAdapter<Item> {
+ injectTheme(items)
+ return super.setNewList(items)
+ }
+
+ override fun <T, S> setSubItems(collapsible: T, subItems: MutableList<S>?): T where S : IItem<*, *>?, T : IItem<*, *>?, T : IExpandable<T, S>?, S : ISubItem<Item, T>? {
+ injectTheme(subItems)
+ return super.setSubItems(collapsible, subItems)
+ }
+
+ internal fun injectTheme(items: Collection<IItem<*, *>?>?) {
+ items?.forEach { injectTheme(it) }
+ }
+
+ internal fun injectTheme(item: IItem<*, *>?) {
+ if (item is ThemableIItem && item.themeEnabled) {
+ item.textColor = textColor
+ item.backgroundColor = backgroundColor
+ item.accentColor = accentColor
+ }
+ }
+}
+
+interface ThemableIItemColors {
+ var textColor: Int?
+ var backgroundColor: Int?
+ var accentColor: Int?
+}
+
+class ThemableIItemColorsDelegate : ThemableIItemColors {
+ override var textColor: Int? = null
+ override var backgroundColor: Int? = null
+ override var accentColor: Int? = null
+}
+
+/**
+ * Interface that needs to be implemented by every iitem
+ * Holds the color values and has helper methods to inject the colors
+ */
+interface ThemableIItem : ThemableIItemColors {
+ var themeEnabled: Boolean
+ fun bindTextColor(vararg views: TextView?)
+ fun bindTextColorSecondary(vararg views: TextView?)
+ fun bindDividerColor(vararg views: View?)
+ fun bindAccentColor(vararg views: TextView?)
+ fun bindBackgroundColor(vararg views: View?)
+ fun bindBackgroundRipple(vararg views: View?)
+ fun bindIconColor(vararg views: ImageView?)
+}
+
+/**
+ * The delegate for [ThemableIItem]
+ */
+class ThemableIItemDelegate : ThemableIItem, ThemableIItemColors by ThemableIItemColorsDelegate() {
+ override var themeEnabled: Boolean = true
+
+ override fun bindTextColor(vararg views: TextView?) {
+ val color = textColor ?: return
+ views.forEach { it?.setTextColor(color) }
+ }
+
+ override fun bindTextColorSecondary(vararg views: TextView?) {
+ val color = textColor?.adjustAlpha(0.8f) ?: return
+ views.forEach { it?.setTextColor(color) }
+ }
+
+ override fun bindAccentColor(vararg views: TextView?) {
+ val color = accentColor ?: textColor ?: return
+ views.forEach { it?.setTextColor(color) }
+ }
+
+ override fun bindDividerColor(vararg views: View?) {
+ val color = (textColor ?: accentColor)?.adjustAlpha(0.1f) ?: return
+ views.forEach { it?.setBackgroundColor(color) }
+ }
+
+ override fun bindBackgroundColor(vararg views: View?) {
+ val color = backgroundColor ?: return
+ views.forEach { it?.setBackgroundColor(color) }
+ }
+
+ override fun bindBackgroundRipple(vararg views: View?) {
+ val foreground = accentColor ?: textColor ?: return
+ val background = backgroundColor ?: return
+ val ripple = createSimpleRippleDrawable(foreground, background)
+ views.forEach { it?.background = ripple }
+ }
+
+ override fun bindIconColor(vararg views: ImageView?) {
+ val color = accentColor ?: textColor ?: return
+ views.forEach { it?.drawable?.setTintList(ColorStateList.valueOf(color)) }
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt b/core/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt
new file mode 100644
index 0000000..cf7205a
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/adapters/SectionAdapter.kt
@@ -0,0 +1,13 @@
+package ca.allanwang.kau.adapters
+
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.adapters.HeaderAdapter
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Extension of [HeaderAdapter] where we can define the order
+ */
+class SectionAdapter<Item : IItem<*, *>>(var sectionOrder: Int = 100) : HeaderAdapter<Item>() {
+ override fun getOrder(): Int = sectionOrder
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt
new file mode 100644
index 0000000..c649376
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/BaseDelayAnimator.kt
@@ -0,0 +1,45 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Base for delayed animators
+ * item delay factor by default can be 0.125f
+ */
+abstract class BaseDelayAnimator(val itemDelayFactor: Float) : DefaultAnimator() {
+
+ override abstract fun addAnimationPrepare(holder: RecyclerView.ViewHolder)
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ startDelay = Math.max(0L, (holder.adapterPosition * addDuration * itemDelayFactor).toLong())
+ duration = this@BaseDelayAnimator.addDuration
+ interpolator = this@BaseDelayAnimator.interpolator
+ }
+ }
+
+
+ override abstract fun addAnimationCleanup(holder: RecyclerView.ViewHolder)
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ /**
+ * Partial removal animation
+ * As of now, all it does it change the alpha
+ * To have it slide, add onto it in a sub class
+ */
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ duration = this@BaseDelayAnimator.removeDuration
+ startDelay = Math.max(0L, (holder.adapterPosition * removeDuration * itemDelayFactor).toLong())
+ interpolator = this@BaseDelayAnimator.interpolator
+ }
+ }
+
+ override abstract fun removeAnimationCleanup(holder: RecyclerView.ViewHolder)
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java b/core/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
new file mode 100644
index 0000000..69c2cf3
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/BaseItemAnimator.java
@@ -0,0 +1,764 @@
+package ca.allanwang.kau.animators;
+
+/*
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Based on Item Animator by {@author Mike Penz}
+ * Rewritten to match with the updated compat dependencies
+ */
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.support.annotation.NonNull;
+import android.support.v4.view.ViewCompat;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.support.v7.widget.SimpleItemAnimator;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.view.animation.Interpolator;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * This implementation of {@link android.support.v7.widget.RecyclerView.ItemAnimator} provides basic
+ * animations on remove, add, and move events that happen to the items in
+ * a RecyclerView. RecyclerView uses a DefaultItemAnimator by default.
+ *
+ * @see android.support.v7.widget.RecyclerView#setItemAnimator(android.support.v7.widget.RecyclerView.ItemAnimator)
+ */
+public abstract class BaseItemAnimator extends SimpleItemAnimator {
+ private static final boolean DEBUG = false;
+
+ private static TimeInterpolator sDefaultInterpolator;
+
+ private ArrayList<ViewHolder> mPendingRemovals = new ArrayList<>();
+ private ArrayList<ViewHolder> mPendingAdditions = new ArrayList<>();
+ private ArrayList<MoveInfo> mPendingMoves = new ArrayList<>();
+ private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<>();
+
+ ArrayList<ArrayList<ViewHolder>> mAdditionsList = new ArrayList<>();
+ ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<>();
+ ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<>();
+
+ ArrayList<ViewHolder> mAddAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mMoveAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mRemoveAnimations = new ArrayList<>();
+ ArrayList<ViewHolder> mChangeAnimations = new ArrayList<>();
+
+ public Interpolator interpolator;
+
+ private static class MoveInfo {
+ public ViewHolder holder;
+ public int fromX, fromY, toX, toY;
+
+ MoveInfo(ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ this.holder = holder;
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+ }
+
+ public static class ChangeInfo {
+ public ViewHolder oldHolder, newHolder;
+ public int fromX, fromY, toX, toY;
+
+ private ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder) {
+ this.oldHolder = oldHolder;
+ this.newHolder = newHolder;
+ }
+
+ ChangeInfo(ViewHolder oldHolder, ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ this(oldHolder, newHolder);
+ this.fromX = fromX;
+ this.fromY = fromY;
+ this.toX = toX;
+ this.toY = toY;
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeInfo{"
+ + "oldHolder=" + oldHolder
+ + ", newHolder=" + newHolder
+ + ", fromX=" + fromX
+ + ", fromY=" + fromY
+ + ", toX=" + toX
+ + ", toY=" + toY
+ + '}';
+ }
+ }
+
+ @Override
+ public void runPendingAnimations() {
+ boolean removalsPending = !mPendingRemovals.isEmpty();
+ boolean movesPending = !mPendingMoves.isEmpty();
+ boolean changesPending = !mPendingChanges.isEmpty();
+ boolean additionsPending = !mPendingAdditions.isEmpty();
+ if (!removalsPending && !movesPending && !additionsPending && !changesPending) {
+ // nothing to animate
+ return;
+ }
+ // First, remove stuff
+ for (ViewHolder holder : mPendingRemovals) {
+ animateRemoveImpl(holder);
+ }
+ mPendingRemovals.clear();
+ // Next, move stuff
+ if (movesPending) {
+ final ArrayList<MoveInfo> moves = new ArrayList<>();
+ moves.addAll(mPendingMoves);
+ mMovesList.add(moves);
+ mPendingMoves.clear();
+ Runnable mover = new Runnable() {
+ @Override
+ public void run() {
+ for (MoveInfo moveInfo : moves) {
+ animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY,
+ moveInfo.toX, moveInfo.toY);
+ }
+ moves.clear();
+ mMovesList.remove(moves);
+ }
+ };
+ if (removalsPending) {
+ View view = moves.get(0).holder.itemView;
+ ViewCompat.postOnAnimationDelayed(view, mover, 0);
+ } else {
+ mover.run();
+ }
+ }
+ // Next, change stuff, to run in parallel with move animations
+ if (changesPending) {
+ final ArrayList<ChangeInfo> changes = new ArrayList<>();
+ changes.addAll(mPendingChanges);
+ mChangesList.add(changes);
+ mPendingChanges.clear();
+ Runnable changer = new Runnable() {
+ @Override
+ public void run() {
+ for (ChangeInfo change : changes) {
+ animateChangeImpl(change);
+ }
+ changes.clear();
+ mChangesList.remove(changes);
+ }
+ };
+ if (removalsPending) {
+ ViewHolder holder = changes.get(0).oldHolder;
+ long moveDuration = movesPending ? getMoveDuration() : 0;
+ ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDelay(getRemoveDuration(), moveDuration, getChangeDuration()));
+ } else {
+ changer.run();
+ }
+ }
+ // Next, add stuff
+ if (additionsPending) {
+ final ArrayList<ViewHolder> additions = new ArrayList<>();
+ additions.addAll(mPendingAdditions);
+ mAdditionsList.add(additions);
+ mPendingAdditions.clear();
+ Runnable adder = new Runnable() {
+ @Override
+ public void run() {
+ for (ViewHolder holder : additions) {
+ animateAddImpl(holder);
+ }
+ additions.clear();
+ mAdditionsList.remove(additions);
+ }
+ };
+ if (removalsPending || movesPending || changesPending) {
+ long removeDuration = removalsPending ? getRemoveDuration() : 0;
+ long moveDuration = movesPending ? getMoveDuration() : 0;
+ long changeDuration = changesPending ? getChangeDuration() : 0;
+ View view = additions.get(0).itemView;
+ ViewCompat.postOnAnimationDelayed(view, adder, getAddDelay(removeDuration, moveDuration, changeDuration));
+ } else {
+ adder.run();
+ }
+ }
+ }
+
+ /**
+ * used to calculated the delay until the remove animation should start
+ *
+ * @param remove the remove duration
+ * @param move the move duration
+ * @param change the change duration
+ * @return the calculated delay for the remove items animation
+ */
+ public long getRemoveDelay(long remove, long move, long change) {
+ return remove + Math.max(move, change);
+ }
+
+ /**
+ * used to calculated the delay until the add animation should start
+ *
+ * @param remove the remove duration
+ * @param move the move duration
+ * @param change the change duration
+ * @return the calculated delay for the add items animation
+ */
+ public long getAddDelay(long remove, long move, long change) {
+ return remove + Math.max(move, change);
+ }
+
+ @Override
+ public boolean animateRemove(final ViewHolder holder) {
+ resetAnimation(holder);
+ mPendingRemovals.add(holder);
+ return true;
+ }
+
+ private void animateRemoveImpl(final ViewHolder holder) {
+ final ViewPropertyAnimator animation = removeAnimation(holder);
+ mRemoveAnimations.add(holder);
+ animation.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchRemoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ removeAnimationCleanup(holder);
+ dispatchRemoveFinished(holder);
+ mRemoveAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ abstract public ViewPropertyAnimator removeAnimation(ViewHolder holder);
+
+ abstract public void removeAnimationCleanup(ViewHolder holder);
+
+ @Override
+ public boolean animateAdd(final ViewHolder holder) {
+ resetAnimation(holder);
+ addAnimationPrepare(holder);
+ mPendingAdditions.add(holder);
+ return true;
+ }
+
+ void animateAddImpl(final ViewHolder holder) {
+ final ViewPropertyAnimator animation = addAnimation(holder);
+ mAddAnimations.add(holder);
+ animation.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchAddStarting(holder);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ addAnimationCleanup(holder);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ dispatchAddFinished(holder);
+ mAddAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ addAnimationCleanup(holder);
+ }
+ }).start();
+ }
+
+ /**
+ * the animation to prepare the view before the add animation is run
+ *
+ * @param holder
+ */
+ public abstract void addAnimationPrepare(ViewHolder holder);
+
+ /**
+ * the animation for adding a view
+ *
+ * @param holder
+ * @return
+ */
+ public abstract ViewPropertyAnimator addAnimation(ViewHolder holder);
+
+ /**
+ * the cleanup method if the animation needs to be stopped. and tro prepare for the next view
+ *
+ * @param holder
+ */
+ abstract void addAnimationCleanup(ViewHolder holder);
+
+ @Override
+ public boolean animateMove(final ViewHolder holder, int fromX, int fromY,
+ int toX, int toY) {
+ final View view = holder.itemView;
+ fromX += (int) holder.itemView.getTranslationX();
+ fromY += (int) holder.itemView.getTranslationY();
+ resetAnimation(holder);
+ int deltaX = toX - fromX;
+ int deltaY = toY - fromY;
+ if (deltaX == 0 && deltaY == 0) {
+ dispatchMoveFinished(holder);
+ return false;
+ }
+ if (deltaX != 0) {
+ view.setTranslationX(-deltaX);
+ }
+ if (deltaY != 0) {
+ view.setTranslationY(-deltaY);
+ }
+ mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY));
+ return true;
+ }
+
+ void animateMoveImpl(final ViewHolder holder, int fromX, int fromY, int toX, int toY) {
+ final View view = holder.itemView;
+ final int deltaX = toX - fromX;
+ final int deltaY = toY - fromY;
+ if (deltaX != 0) {
+ view.animate().translationX(0);
+ }
+ if (deltaY != 0) {
+ view.animate().translationY(0);
+ }
+ // TODO: make EndActions end listeners instead, since end actions aren't called when
+ // vpas are canceled (and can't end them. why?)
+ // need listener functionality in VPACompat for this. Ick.
+ final ViewPropertyAnimator animation = view.animate();
+ mMoveAnimations.add(holder);
+ animation.setDuration(getMoveDuration()).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchMoveStarting(holder);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+ if (deltaX != 0) {
+ view.setTranslationX(0);
+ }
+ if (deltaY != 0) {
+ view.setTranslationY(0);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ animation.setListener(null);
+ dispatchMoveFinished(holder);
+ mMoveAnimations.remove(holder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+
+ @Override
+ public boolean animateChange(ViewHolder oldHolder, ViewHolder newHolder,
+ int fromX, int fromY, int toX, int toY) {
+ if (oldHolder == newHolder) {
+ // Don't know how to run change animations when the same view holder is re-used.
+ // run a move animation to handle position changes.
+ return animateMove(oldHolder, fromX, fromY, toX, toY);
+ }
+ changeAnimation(oldHolder, newHolder,
+ fromX, fromY, toX, toY);
+ mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY));
+ return true;
+ }
+
+ void animateChangeImpl(final ChangeInfo changeInfo) {
+ final ViewHolder holder = changeInfo.oldHolder;
+ final View view = holder == null ? null : holder.itemView;
+ final ViewHolder newHolder = changeInfo.newHolder;
+ final View newView = newHolder != null ? newHolder.itemView : null;
+ if (view != null) {
+ final ViewPropertyAnimator oldViewAnim = changeOldAnimation(holder, changeInfo);
+ mChangeAnimations.add(changeInfo.oldHolder);
+ oldViewAnim.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(changeInfo.oldHolder, true);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ oldViewAnim.setListener(null);
+ changeAnimationCleanup(holder);
+ view.setTranslationX(0);
+ view.setTranslationY(0);
+ dispatchChangeFinished(changeInfo.oldHolder, true);
+ mChangeAnimations.remove(changeInfo.oldHolder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+ if (newView != null) {
+ final ViewPropertyAnimator newViewAnimation = changeNewAnimation(newHolder);
+ mChangeAnimations.add(changeInfo.newHolder);
+ newViewAnimation.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+ dispatchChangeStarting(changeInfo.newHolder, false);
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ newViewAnimation.setListener(null);
+ changeAnimationCleanup(newHolder);
+ newView.setTranslationX(0);
+ newView.setTranslationY(0);
+ dispatchChangeFinished(changeInfo.newHolder, false);
+ mChangeAnimations.remove(changeInfo.newHolder);
+ dispatchFinishedWhenDone();
+ }
+ }).start();
+ }
+ }
+
+ /**
+ * the whole change animation if we have to cross animate two views
+ *
+ * @param oldHolder
+ * @param newHolder
+ * @param fromX
+ * @param fromY
+ * @param toX
+ * @param toY
+ */
+ public void changeAnimation(ViewHolder oldHolder, ViewHolder newHolder, int fromX, int fromY, int toX, int toY) {
+ final float prevTranslationX = oldHolder.itemView.getTranslationX();
+ final float prevTranslationY = oldHolder.itemView.getTranslationY();
+ final float prevValue = oldHolder.itemView.getAlpha();
+ resetAnimation(oldHolder);
+ int deltaX = (int) (toX - fromX - prevTranslationX);
+ int deltaY = (int) (toY - fromY - prevTranslationY);
+ // recover prev translation state after ending animation
+ oldHolder.itemView.setTranslationX(prevTranslationX);
+ oldHolder.itemView.setTranslationY(prevTranslationY);
+
+ oldHolder.itemView.setAlpha(prevValue);
+ if (newHolder != null) {
+ // carry over translation values
+ resetAnimation(newHolder);
+ newHolder.itemView.setTranslationX(-deltaX);
+ newHolder.itemView.setTranslationY(-deltaY);
+ newHolder.itemView.setAlpha(0);
+ }
+ }
+
+ /**
+ * the animation for removing the old view
+ *
+ * @param holder
+ * @return
+ */
+ public abstract ViewPropertyAnimator changeOldAnimation(ViewHolder holder, ChangeInfo changeInfo);
+
+ /**
+ * the animation for changing the new view
+ *
+ * @param holder
+ * @return
+ */
+ public abstract ViewPropertyAnimator changeNewAnimation(ViewHolder holder);
+
+ /**
+ * the cleanup method if the animation needs to be stopped. and tro prepare for the next view
+ *
+ * @param holder
+ */
+ public abstract void changeAnimationCleanup(ViewHolder holder);
+
+ private void endChangeAnimation(List<ChangeInfo> infoList, ViewHolder item) {
+ for (int i = infoList.size() - 1; i >= 0; i--) {
+ ChangeInfo changeInfo = infoList.get(i);
+ if (endChangeAnimationIfNecessary(changeInfo, item)) {
+ if (changeInfo.oldHolder == null && changeInfo.newHolder == null) {
+ infoList.remove(changeInfo);
+ }
+ }
+ }
+ }
+
+ private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) {
+ if (changeInfo.oldHolder != null) {
+ endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder);
+ }
+ if (changeInfo.newHolder != null) {
+ endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder);
+ }
+ }
+
+ private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, ViewHolder item) {
+ boolean oldItem = false;
+ if (changeInfo.newHolder == item) {
+ changeInfo.newHolder = null;
+ } else if (changeInfo.oldHolder == item) {
+ changeInfo.oldHolder = null;
+ oldItem = true;
+ } else {
+ return false;
+ }
+ changeAnimationCleanup(item);
+ item.itemView.setTranslationX(0);
+ item.itemView.setTranslationY(0);
+ dispatchChangeFinished(item, oldItem);
+ return true;
+ }
+
+ @Override
+ public void endAnimation(ViewHolder item) {
+ final View view = item.itemView;
+ // this will trigger end callback which should set properties to their target values.
+ view.animate().cancel();
+ // TODO if some other animations are chained to end, how do we cancel them as well?
+ for (int i = mPendingMoves.size() - 1; i >= 0; i--) {
+ MoveInfo moveInfo = mPendingMoves.get(i);
+ if (moveInfo.holder == item) {
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item);
+ mPendingMoves.remove(i);
+ }
+ }
+ endChangeAnimation(mPendingChanges, item);
+ if (mPendingRemovals.remove(item)) {
+ removeAnimationCleanup(item);
+ dispatchRemoveFinished(item);
+ }
+ if (mPendingAdditions.remove(item)) {
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ }
+
+ for (int i = mChangesList.size() - 1; i >= 0; i--) {
+ ArrayList<ChangeInfo> changes = mChangesList.get(i);
+ endChangeAnimation(changes, item);
+ if (changes.isEmpty()) {
+ mChangesList.remove(i);
+ }
+ }
+ for (int i = mMovesList.size() - 1; i >= 0; i--) {
+ ArrayList<MoveInfo> moves = mMovesList.get(i);
+ for (int j = moves.size() - 1; j >= 0; j--) {
+ MoveInfo moveInfo = moves.get(j);
+ if (moveInfo.holder == item) {
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item);
+ moves.remove(j);
+ if (moves.isEmpty()) {
+ mMovesList.remove(i);
+ }
+ break;
+ }
+ }
+ }
+ for (int i = mAdditionsList.size() - 1; i >= 0; i--) {
+ ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+ if (additions.remove(item)) {
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ if (additions.isEmpty()) {
+ mAdditionsList.remove(i);
+ }
+ }
+ }
+
+ // animations should be ended by the cancel above.
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mRemoveAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mRemoveAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mAddAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mAddAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mChangeAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mChangeAnimations list");
+ }
+
+ //noinspection PointlessBooleanExpression,ConstantConditions
+ if (mMoveAnimations.remove(item) && DEBUG) {
+ throw new IllegalStateException("after animation is cancelled, item should not be in "
+ + "mMoveAnimations list");
+ }
+ dispatchFinishedWhenDone();
+ }
+
+ private void resetAnimation(ViewHolder holder) {
+
+ if (sDefaultInterpolator == null) {
+ sDefaultInterpolator = new ValueAnimator().getInterpolator();
+ }
+ holder.itemView.animate().setInterpolator(sDefaultInterpolator);
+ endAnimation(holder);
+ }
+
+ @Override
+ public boolean isRunning() {
+ return (!mPendingAdditions.isEmpty()
+ || !mPendingChanges.isEmpty()
+ || !mPendingMoves.isEmpty()
+ || !mPendingRemovals.isEmpty()
+ || !mMoveAnimations.isEmpty()
+ || !mRemoveAnimations.isEmpty()
+ || !mAddAnimations.isEmpty()
+ || !mChangeAnimations.isEmpty()
+ || !mMovesList.isEmpty()
+ || !mAdditionsList.isEmpty()
+ || !mChangesList.isEmpty());
+ }
+
+ /**
+ * Check the state of currently pending and running animations. If there are none
+ * pending/running, call {@link #dispatchAnimationsFinished()} to notify any
+ * listeners.
+ */
+ void dispatchFinishedWhenDone() {
+ if (!isRunning()) {
+ dispatchAnimationsFinished();
+ }
+ }
+
+ @Override
+ public void endAnimations() {
+ int count = mPendingMoves.size();
+ for (int i = count - 1; i >= 0; i--) {
+ MoveInfo item = mPendingMoves.get(i);
+ View view = item.holder.itemView;
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(item.holder);
+ mPendingMoves.remove(i);
+ }
+ count = mPendingRemovals.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingRemovals.get(i);
+ dispatchRemoveFinished(item);
+ mPendingRemovals.remove(i);
+ }
+ count = mPendingAdditions.size();
+ for (int i = count - 1; i >= 0; i--) {
+ ViewHolder item = mPendingAdditions.get(i);
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ mPendingAdditions.remove(i);
+ }
+ count = mPendingChanges.size();
+ for (int i = count - 1; i >= 0; i--) {
+ endChangeAnimationIfNecessary(mPendingChanges.get(i));
+ }
+ mPendingChanges.clear();
+ if (!isRunning()) {
+ return;
+ }
+
+ int listCount = mMovesList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<MoveInfo> moves = mMovesList.get(i);
+ count = moves.size();
+ for (int j = count - 1; j >= 0; j--) {
+ MoveInfo moveInfo = moves.get(j);
+ ViewHolder item = moveInfo.holder;
+ View view = item.itemView;
+ view.setTranslationY(0);
+ view.setTranslationX(0);
+ dispatchMoveFinished(moveInfo.holder);
+ moves.remove(j);
+ if (moves.isEmpty()) {
+ mMovesList.remove(moves);
+ }
+ }
+ }
+ listCount = mAdditionsList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<ViewHolder> additions = mAdditionsList.get(i);
+ count = additions.size();
+ for (int j = count - 1; j >= 0; j--) {
+ ViewHolder item = additions.get(j);
+ View view = item.itemView;
+ addAnimationCleanup(item);
+ dispatchAddFinished(item);
+ additions.remove(j);
+ if (additions.isEmpty()) {
+ mAdditionsList.remove(additions);
+ }
+ }
+ }
+ listCount = mChangesList.size();
+ for (int i = listCount - 1; i >= 0; i--) {
+ ArrayList<ChangeInfo> changes = mChangesList.get(i);
+ count = changes.size();
+ for (int j = count - 1; j >= 0; j--) {
+ endChangeAnimationIfNecessary(changes.get(j));
+ if (changes.isEmpty()) {
+ mChangesList.remove(changes);
+ }
+ }
+ }
+
+ cancelAll(mRemoveAnimations);
+ cancelAll(mMoveAnimations);
+ cancelAll(mAddAnimations);
+ cancelAll(mChangeAnimations);
+
+ dispatchAnimationsFinished();
+ }
+
+ void cancelAll(List<ViewHolder> viewHolders) {
+ for (int i = viewHolders.size() - 1; i >= 0; i--) {
+ viewHolders.get(i).itemView.animate().cancel();
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * If the payload list is not empty, DefaultItemAnimator returns <code>true</code>.
+ * When this is the case:
+ * <ul>
+ * <li>If you override {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)}, both
+ * ViewHolder arguments will be the same instance.
+ * </li>
+ * <li>
+ * If you are not overriding {@link #animateChange(ViewHolder, ViewHolder, int, int, int, int)},
+ * then DefaultItemAnimator will call {@link #animateMove(ViewHolder, int, int, int, int)} and
+ * run a move animation instead.
+ * </li>
+ * </ul>
+ */
+ @Override
+ public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
+ @NonNull List<Object> payloads) {
+ return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
+ }
+}
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt
new file mode 100644
index 0000000..a963358
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/BaseSlideAlphaAnimator.kt
@@ -0,0 +1,52 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Base for sliding animators
+ * item delay factor by default can be 0.125f
+ */
+abstract class BaseSlideAlphaAnimator(itemDelayFactor: Float) : BaseDelayAnimator(itemDelayFactor) {
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.addAnimation(holder).apply {
+ translationY(0f)
+ translationX(0f)
+ alpha(1f)
+ }
+ }
+
+ final override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+ }
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ /**
+ * Partial removal animation
+ * As of now, all it does it change the alpha
+ * To have it slide, add onto it in a sub class
+ */
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.addAnimation(holder).apply {
+ alpha(0f)
+ }
+ }
+
+ override final fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
new file mode 100644
index 0000000..9aeafde
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/DefaultAnimator.kt
@@ -0,0 +1,63 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ */
+open class DefaultAnimator : BaseItemAnimator() {
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(0f)
+ duration = this@DefaultAnimator.removeDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 1f
+ }
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 0f
+ }
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(1f)
+ duration = this@DefaultAnimator.addDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 1f
+ }
+
+ override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(0f)
+ translationX(changeInfo.toX.toFloat() - changeInfo.fromX)
+ translationY(changeInfo.toY.toFloat() - changeInfo.fromY)
+ duration = this@DefaultAnimator.changeDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return holder.itemView.animate().apply {
+ alpha(1f)
+ translationX(0f)
+ translationY(0f)
+ duration = this@DefaultAnimator.changeDuration
+ interpolator = this@DefaultAnimator.interpolator
+ }
+ }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ holder.itemView.alpha = 1f
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt
new file mode 100644
index 0000000..e968cda
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/FadeScaleAnimator.kt
@@ -0,0 +1,51 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-29.
+ */
+open class FadeScaleAnimator(val scaleFactor: Float = 0.7f, itemDelayFactor: Float = 0.125f) : BaseDelayAnimator(itemDelayFactor) {
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ scaleX = scaleFactor
+ scaleY = scaleFactor
+ alpha = 0f
+ }
+ }
+
+ override final fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.addAnimation(holder).apply {
+ scaleX(1f)
+ scaleY(1f)
+ alpha(1f)
+ }
+ }
+
+
+ final override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ scaleX = 1f
+ scaleY = 1f
+ alpha = 1f
+ }
+ }
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.removeAnimation(holder).apply {
+ scaleX(scaleFactor)
+ scaleY(scaleFactor)
+ alpha(0f)
+ }
+ }
+
+ override final fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = 0f
+ translationX = 0f
+ alpha = 1f
+ }
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
new file mode 100644
index 0000000..244287b
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/NoAnimator.kt
@@ -0,0 +1,41 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ *
+ * Truly have no animation
+ */
+class NoAnimator : DefaultAnimator() {
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {}
+
+ override fun addAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun addAnimationCleanup(holder: RecyclerView.ViewHolder) {}
+
+ override fun changeOldAnimation(holder: RecyclerView.ViewHolder, changeInfo: ChangeInfo): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun changeNewAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun changeAnimationCleanup(holder: RecyclerView.ViewHolder) {}
+
+ override fun changeAnimation(oldHolder: RecyclerView.ViewHolder, newHolder: RecyclerView.ViewHolder?, fromX: Int, fromY: Int, toX: Int, toY: Int) {}
+
+ override fun getAddDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun getAddDuration(): Long = 0
+
+ override fun getMoveDuration(): Long = 0
+
+ override fun getRemoveDuration(): Long = 0
+
+ override fun getChangeDuration(): Long = 0
+
+ override fun getRemoveDelay(remove: Long, move: Long, change: Long): Long = 0
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator = holder.itemView.animate().apply { duration = 0 }
+
+ override fun removeAnimationCleanup(holder: RecyclerView.ViewHolder) {}
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt b/core/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt
new file mode 100644
index 0000000..8670493
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/animators/SlideUpExitRightAnimator.kt
@@ -0,0 +1,23 @@
+package ca.allanwang.kau.animators
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewPropertyAnimator
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ */
+class SlideUpExitRightAnimator(itemDelayFactor: Float = 0.125f) : BaseSlideAlphaAnimator(itemDelayFactor) {
+
+ override fun addAnimationPrepare(holder: RecyclerView.ViewHolder) {
+ with(holder.itemView) {
+ translationY = height.toFloat()
+ alpha = 0f
+ }
+ }
+
+ override fun removeAnimation(holder: RecyclerView.ViewHolder): ViewPropertyAnimator {
+ return super.removeAnimation(holder).apply {
+ translationX(holder.itemView.width.toFloat())
+ }
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt b/core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt
new file mode 100644
index 0000000..302d9dc
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt
@@ -0,0 +1,105 @@
+package ca.allanwang.kau.changelog
+
+import android.content.Context
+import android.content.res.XmlResourceParser
+import android.os.Handler
+import android.support.annotation.ColorInt
+import android.support.annotation.LayoutRes
+import android.support.annotation.XmlRes
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.bindOptionalView
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.materialDialog
+import ca.allanwang.kau.utils.use
+import com.afollestad.materialdialogs.MaterialDialog
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
+import org.xmlpull.v1.XmlPullParser
+import java.util.*
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+
+fun Context.showChangelog(@XmlRes xmlRes: Int, @ColorInt textColor: Int? = null, customize: MaterialDialog.Builder.() -> Unit = {}) {
+ doAsync {
+ val items = parse(this@showChangelog, xmlRes)
+ uiThread {
+ materialDialog {
+ title(R.string.kau_changelog)
+ positiveText(R.string.kau_great)
+ adapter(ChangelogAdapter(items, textColor), null)
+ customize()
+ }
+ }
+ }
+}
+
+/**
+ * Internals of the changelog dialog
+ * Contains an mainAdapter for each item, as well as the tags to parse
+ */
+internal class ChangelogAdapter(val items: List<Pair<String, ChangelogType>>, @ColorInt val textColor: Int? = null) : RecyclerView.Adapter<ChangelogAdapter.ChangelogVH>() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ChangelogVH(LayoutInflater.from(parent.context)
+ .inflate(items[viewType].second.layout, parent, false))
+
+ override fun onBindViewHolder(holder: ChangelogVH, position: Int) {
+ holder.text.text = items[position].first
+ if (textColor != null) {
+ holder.text.setTextColor(textColor)
+ holder.bullet?.setTextColor(textColor)
+ }
+ }
+
+ override fun getItemId(position: Int) = position.toLong()
+
+ override fun getItemViewType(position: Int) = position
+
+ override fun getItemCount() = items.size
+
+ internal class ChangelogVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val text: TextView by bindView(R.id.kau_changelog_text)
+ val bullet: TextView? by bindOptionalView(R.id.kau_changelog_bullet)
+ }
+}
+
+internal fun parse(context: Context, @XmlRes xmlRes: Int): List<Pair<String, ChangelogType>> {
+ val items = mutableListOf<Pair<String, ChangelogType>>()
+ context.resources.getXml(xmlRes).use {
+ parser: XmlResourceParser ->
+ var eventType = parser.eventType
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG)
+ ChangelogType.values.any { it.add(parser, items) }
+ eventType = parser.next()
+ }
+ }
+ return items
+}
+
+internal enum class ChangelogType(val tag: String, val attr: String, @LayoutRes val layout: Int) {
+ TITLE("version", "title", R.layout.kau_changelog_title),
+ ITEM("item", "text", R.layout.kau_changelog_content);
+
+ companion object {
+ @JvmStatic val values = values()
+ }
+
+ /**
+ * Returns true if tag matches; false otherwise
+ */
+ fun add(parser: XmlResourceParser, list: MutableList<Pair<String, ChangelogType>>): Boolean {
+ if (parser.name != tag) return false
+ if (parser.getAttributeValue(null, attr).isNotBlank())
+ list.add(Pair(parser.getAttributeValue(null, attr), this))
+ return true
+ }
+}
+
diff --git a/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt
new file mode 100644
index 0000000..3430b42
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt
@@ -0,0 +1,228 @@
+package ca.allanwang.kau.dialogs.color
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.StateListDrawable
+import android.graphics.drawable.shapes.OvalShape
+import android.os.Build
+import android.support.annotation.ColorInt
+import android.support.annotation.ColorRes
+import android.support.annotation.FloatRange
+import android.support.v4.view.GravityCompat
+import android.support.v4.view.ViewCompat
+import android.util.AttributeSet
+import android.view.Gravity
+import android.widget.FrameLayout
+import android.widget.Toast
+import ca.allanwang.kau.utils.color
+import ca.allanwang.kau.utils.getDip
+import ca.allanwang.kau.utils.toColor
+import ca.allanwang.kau.utils.toHSV
+
+/**
+ * Created by Allan Wang on 2017-06-10.
+ *
+ * An extension of MaterialDialog's CircleView with animation selection
+ * [https://github.com/afollestad/material-dialogs/blob/master/commons/src/main/java/com/afollestad/materialdialogs/color/CircleView.java]
+ */
+class CircleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val borderWidthMicro: Float = context.getDip(1f)
+ private val borderWidthSmall: Float = context.getDip(3f)
+ private val borderWidthLarge: Float = context.getDip(5f)
+ private var whiteOuterBound: Float = borderWidthLarge
+
+ private val outerPaint: Paint = Paint().apply { isAntiAlias = true }
+ private val whitePaint: Paint = Paint().apply { isAntiAlias = true; color = Color.WHITE }
+ private val innerPaint: Paint = Paint().apply { isAntiAlias = true }
+ private var selected: Boolean = false
+ var withBorder: Boolean = false
+ get() = field
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidate()
+ }
+ }
+
+ init {
+ update(Color.DKGRAY)
+ setWillNotDraw(false)
+ }
+
+ private fun update(@ColorInt color: Int) {
+ innerPaint.color = color
+ outerPaint.color = shiftColorDown(color)
+
+ val selector = createSelector(color)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val states = arrayOf(intArrayOf(android.R.attr.state_pressed))
+ val colors = intArrayOf(shiftColorUp(color))
+ val rippleColors = ColorStateList(states, colors)
+ foreground = RippleDrawable(rippleColors, selector, null)
+ } else {
+ foreground = selector
+ }
+ }
+
+ override fun setBackgroundColor(@ColorInt color: Int) {
+ update(color)
+ requestLayout()
+ invalidate()
+ }
+
+ override fun setBackgroundResource(@ColorRes color: Int) {
+ setBackgroundColor(context.color(color))
+ }
+
+
+ @Deprecated("")
+ override fun setBackground(background: Drawable) {
+ throw IllegalStateException("Cannot use setBackground() on CircleView.")
+ }
+
+
+ @Deprecated("")
+ override fun setBackgroundDrawable(background: Drawable) {
+ throw IllegalStateException("Cannot use setBackgroundDrawable() on CircleView.")
+ }
+
+
+ @Deprecated("")
+ override fun setActivated(activated: Boolean) {
+ throw IllegalStateException("Cannot use setActivated() on CircleView.")
+ }
+
+ override fun setSelected(selected: Boolean) {
+ this.selected = selected
+ whiteOuterBound = borderWidthLarge
+ invalidate()
+ }
+
+ fun animateSelected(selected: Boolean) {
+ if (this.selected == selected) return
+ this.selected = true // We need to draw the other bands
+ val range = if (selected) Pair(-borderWidthSmall, borderWidthLarge) else Pair(borderWidthLarge, -borderWidthSmall)
+ val animator = ValueAnimator.ofFloat(range.first, range.second)
+ with(animator) {
+ reverse()
+ duration = 150L
+ addUpdateListener { animation ->
+ whiteOuterBound = animation.animatedValue as Float
+ invalidate()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ this@CircleView.selected = selected
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ this@CircleView.selected = selected
+ }
+ })
+ start()
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec)
+ setMeasuredDimension(measuredWidth, measuredWidth)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ val centerWidth = (measuredWidth / 2).toFloat()
+ val centerHeight = (measuredHeight / 2).toFloat()
+ if (withBorder) canvas.drawCircle(centerWidth, centerHeight, centerWidth, whitePaint)
+ if (selected) {
+ val whiteRadius = centerWidth - whiteOuterBound
+ val innerRadius = whiteRadius - borderWidthSmall
+ if (whiteRadius >= centerWidth) {
+ canvas.drawCircle(centerWidth, centerHeight, centerWidth, whitePaint)
+ } else {
+ canvas.drawCircle(centerWidth, centerHeight, if (withBorder) centerWidth - borderWidthMicro else centerWidth, outerPaint)
+ canvas.drawCircle(centerWidth, centerHeight, whiteRadius, whitePaint)
+ }
+ canvas.drawCircle(centerWidth, centerHeight, innerRadius, innerPaint)
+ } else {
+ canvas.drawCircle(centerWidth, centerHeight, if (withBorder) centerWidth - borderWidthMicro else centerWidth, innerPaint)
+ }
+ }
+
+ private fun createSelector(color: Int): Drawable {
+ val darkerCircle = ShapeDrawable(OvalShape())
+ darkerCircle.paint.color = translucentColor(shiftColorUp(color))
+ val stateListDrawable = StateListDrawable()
+ stateListDrawable.addState(intArrayOf(android.R.attr.state_pressed), darkerCircle)
+ return stateListDrawable
+ }
+
+ fun showHint(color: Int) {
+ val screenPos = IntArray(2)
+ val displayFrame = Rect()
+ getLocationOnScreen(screenPos)
+ getWindowVisibleDisplayFrame(displayFrame)
+ val context = context
+ val width = width
+ val height = height
+ val midy = screenPos[1] + height / 2
+ var referenceX = screenPos[0] + width / 2
+ if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
+ val screenWidth = context.resources.displayMetrics.widthPixels
+ referenceX = screenWidth - referenceX // mirror
+ }
+ val cheatSheet = Toast
+ .makeText(context, String.format("#%06X", 0xFFFFFF and color), Toast.LENGTH_SHORT)
+ if (midy < displayFrame.height()) {
+ // Show along the top; follow action buttons
+ cheatSheet.setGravity(Gravity.TOP or GravityCompat.END, referenceX,
+ screenPos[1] + height - displayFrame.top)
+ } else {
+ // Show along the bottom center
+ cheatSheet.setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, height)
+ }
+ cheatSheet.show()
+ }
+
+ companion object {
+
+ @ColorInt
+ @JvmStatic
+ private fun translucentColor(color: Int): Int {
+ val factor = 0.7f
+ val alpha = Math.round(Color.alpha(color) * factor)
+ val red = Color.red(color)
+ val green = Color.green(color)
+ val blue = Color.blue(color)
+ return Color.argb(alpha, red, green, blue)
+ }
+
+ @ColorInt
+ @JvmStatic
+ fun shiftColor(@ColorInt color: Int,
+ @FloatRange(from = 0.0, to = 2.0) by: Float): Int {
+ if (by == 1f) return color
+ val hsv = color.toHSV()
+ hsv[2] *= by // value component
+ return hsv.toColor()
+ }
+
+ @ColorInt
+ @JvmStatic
+ fun shiftColorDown(@ColorInt color: Int): Int = shiftColor(color, 0.9f)
+
+ @ColorInt
+ @JvmStatic
+ fun shiftColorUp(@ColorInt color: Int): Int = shiftColor(color, 1.1f)
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt
new file mode 100644
index 0000000..22bd0d4
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt
@@ -0,0 +1,349 @@
+package ca.allanwang.kau.dialogs.color
+
+import android.graphics.Color
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+internal object ColorPalette {
+
+ val PRIMARY_COLORS: IntArray by lazy {
+ colorArrayOf(
+ "#F44336",
+ "#E91E63",
+ "#9C27B0",
+ "#673AB7",
+ "#3F51B5",
+ "#2196F3",
+ "#03A9F4",
+ "#00BCD4",
+ "#009688",
+ "#4CAF50",
+ "#8BC34A",
+ "#CDDC39",
+ "#FFEB3B",
+ "#FFC107",
+ "#FF9800",
+ "#FF5722",
+ "#795548",
+ "#9E9E9E",
+ "#607D8B")
+ }
+
+ val PRIMARY_COLORS_SUB: Array<IntArray> by lazy {
+ arrayOf(colorArrayOf(
+ "#FFEBEE",
+ "#FFCDD2",
+ "#EF9A9A",
+ "#E57373",
+ "#EF5350",
+ "#F44336",
+ "#E53935",
+ "#D32F2F",
+ "#C62828",
+ "#B71C1C"
+ ), colorArrayOf(
+ "#FCE4EC",
+ "#F8BBD0",
+ "#F48FB1",
+ "#F06292",
+ "#EC407A",
+ "#E91E63",
+ "#D81B60",
+ "#C2185B",
+ "#AD1457",
+ "#880E4F"
+ ), colorArrayOf(
+ "#F3E5F5",
+ "#E1BEE7",
+ "#CE93D8",
+ "#BA68C8",
+ "#AB47BC",
+ "#9C27B0",
+ "#8E24AA",
+ "#7B1FA2",
+ "#6A1B9A",
+ "#4A148C"
+ ), colorArrayOf(
+ "#EDE7F6",
+ "#D1C4E9",
+ "#B39DDB",
+ "#9575CD",
+ "#7E57C2",
+ "#673AB7",
+ "#5E35B1",
+ "#512DA8",
+ "#4527A0",
+ "#311B92"
+ ), colorArrayOf(
+ "#E8EAF6",
+ "#C5CAE9",
+ "#9FA8DA",
+ "#7986CB",
+ "#5C6BC0",
+ "#3F51B5",
+ "#3949AB",
+ "#303F9F",
+ "#283593",
+ "#1A237E"
+ ), colorArrayOf(
+ "#E3F2FD",
+ "#BBDEFB",
+ "#90CAF9",
+ "#64B5F6",
+ "#42A5F5",
+ "#2196F3",
+ "#1E88E5",
+ "#1976D2",
+ "#1565C0",
+ "#0D47A1"
+ ), colorArrayOf(
+ "#E1F5FE",
+ "#B3E5FC",
+ "#81D4FA",
+ "#4FC3F7",
+ "#29B6F6",
+ "#03A9F4",
+ "#039BE5",
+ "#0288D1",
+ "#0277BD",
+ "#01579B"
+ ), colorArrayOf(
+ "#E0F7FA",
+ "#B2EBF2",
+ "#80DEEA",
+ "#4DD0E1",
+ "#26C6DA",
+ "#00BCD4",
+ "#00ACC1",
+ "#0097A7",
+ "#00838F",
+ "#006064"
+ ), colorArrayOf(
+ "#E0F2F1",
+ "#B2DFDB",
+ "#80CBC4",
+ "#4DB6AC",
+ "#26A69A",
+ "#009688",
+ "#00897B",
+ "#00796B",
+ "#00695C",
+ "#004D40"
+ ), colorArrayOf(
+ "#E8F5E9",
+ "#C8E6C9",
+ "#A5D6A7",
+ "#81C784",
+ "#66BB6A",
+ "#4CAF50",
+ "#43A047",
+ "#388E3C",
+ "#2E7D32",
+ "#1B5E20"
+ ), colorArrayOf(
+ "#F1F8E9",
+ "#DCEDC8",
+ "#C5E1A5",
+ "#AED581",
+ "#9CCC65",
+ "#8BC34A",
+ "#7CB342",
+ "#689F38",
+ "#558B2F",
+ "#33691E"
+ ), colorArrayOf(
+ "#F9FBE7",
+ "#F0F4C3",
+ "#E6EE9C",
+ "#DCE775",
+ "#D4E157",
+ "#CDDC39",
+ "#C0CA33",
+ "#AFB42B",
+ "#9E9D24",
+ "#827717"
+ ), colorArrayOf(
+ "#FFFDE7",
+ "#FFF9C4",
+ "#FFF59D",
+ "#FFF176",
+ "#FFEE58",
+ "#FFEB3B",
+ "#FDD835",
+ "#FBC02D",
+ "#F9A825",
+ "#F57F17"
+ ), colorArrayOf(
+ "#FFF8E1",
+ "#FFECB3",
+ "#FFE082",
+ "#FFD54F",
+ "#FFCA28",
+ "#FFC107",
+ "#FFB300",
+ "#FFA000",
+ "#FF8F00",
+ "#FF6F00"
+ ), colorArrayOf(
+ "#FFF3E0",
+ "#FFE0B2",
+ "#FFCC80",
+ "#FFB74D",
+ "#FFA726",
+ "#FF9800",
+ "#FB8C00",
+ "#F57C00",
+ "#EF6C00",
+ "#E65100"
+ ), colorArrayOf(
+ "#FBE9E7",
+ "#FFCCBC",
+ "#FFAB91",
+ "#FF8A65",
+ "#FF7043",
+ "#FF5722",
+ "#F4511E",
+ "#E64A19",
+ "#D84315",
+ "#BF360C"
+ ), colorArrayOf(
+ "#EFEBE9",
+ "#D7CCC8",
+ "#BCAAA4",
+ "#A1887F",
+ "#8D6E63",
+ "#795548",
+ "#6D4C41",
+ "#5D4037",
+ "#4E342E",
+ "#3E2723"
+ ), colorArrayOf(
+ "#FAFAFA",
+ "#F5F5F5",
+ "#EEEEEE",
+ "#E0E0E0",
+ "#BDBDBD",
+ "#9E9E9E",
+ "#757575",
+ "#616161",
+ "#424242",
+ "#212121"
+ ), colorArrayOf(
+ "#ECEFF1",
+ "#CFD8DC",
+ "#B0BEC5",
+ "#90A4AE",
+ "#78909C",
+ "#607D8B",
+ "#546E7A",
+ "#455A64",
+ "#37474F",
+ "#263238"))
+ }
+
+ val ACCENT_COLORS: IntArray by lazy {
+ colorArrayOf(
+ "#FF1744",
+ "#F50057",
+ "#D500F9",
+ "#651FFF",
+ "#3D5AFE",
+ "#2979FF",
+ "#00B0FF",
+ "#00E5FF",
+ "#1DE9B6",
+ "#00E676",
+ "#76FF03",
+ "#C6FF00",
+ "#FFEA00",
+ "#FFC400",
+ "#FF9100",
+ "#FF3D00")
+ }
+
+ val ACCENT_COLORS_SUB: Array<IntArray> by lazy {
+ arrayOf(colorArrayOf("#FF8A80",
+ "#FF5252",
+ "#FF1744",
+ "#D50000"
+ ), colorArrayOf(
+ "#FF80AB",
+ "#FF4081",
+ "#F50057",
+ "#C51162"
+ ), colorArrayOf(
+ "#EA80FC",
+ "#E040FB",
+ "#D500F9",
+ "#AA00FF"
+ ), colorArrayOf(
+ "#B388FF",
+ "#7C4DFF",
+ "#651FFF",
+ "#6200EA"
+ ), colorArrayOf(
+ "#8C9EFF",
+ "#536DFE",
+ "#3D5AFE",
+ "#304FFE"
+ ), colorArrayOf(
+ "#82B1FF",
+ "#448AFF",
+ "#2979FF",
+ "#2962FF"
+ ), colorArrayOf(
+ "#80D8FF",
+ "#40C4FF",
+ "#00B0FF",
+ "#0091EA"
+ ), colorArrayOf(
+ "#84FFFF",
+ "#18FFFF",
+ "#00E5FF",
+ "#00B8D4"
+ ), colorArrayOf(
+ "#A7FFEB",
+ "#64FFDA",
+ "#1DE9B6",
+ "#00BFA5"
+ ), colorArrayOf(
+ "#B9F6CA",
+ "#69F0AE",
+ "#00E676",
+ "#00C853"
+ ), colorArrayOf(
+ "#CCFF90",
+ "#B2FF59",
+ "#76FF03",
+ "#64DD17"
+ ), colorArrayOf(
+ "#F4FF81",
+ "#EEFF41",
+ "#C6FF00",
+ "#AEEA00"
+ ), colorArrayOf(
+ "#FFFF8D",
+ "#FFFF00",
+ "#FFEA00",
+ "#FFD600"
+ ), colorArrayOf(
+ "#FFE57F",
+ "#FFD740",
+ "#FFC400",
+ "#FFAB00"
+ ), colorArrayOf(
+ "#FFD180",
+ "#FFAB40",
+ "#FF9100",
+ "#FF6D00"
+ ), colorArrayOf(
+ "#FF9E80",
+ "#FF6E40",
+ "#FF3D00",
+ "#DD2C00"))
+ }
+
+ fun colorArrayOf(vararg colors: String) = colors.map { Color.parseColor(it) }.toIntArray()
+}
+
diff --git a/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
new file mode 100644
index 0000000..7c57c26
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
@@ -0,0 +1,82 @@
+package ca.allanwang.kau.dialogs.color
+
+import android.content.Context
+import android.graphics.Color
+import android.support.annotation.DimenRes
+import android.support.annotation.StringRes
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.string
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.Theme
+
+class ColorBuilder : ColorContract {
+ override var title: String? = null
+ override var titleRes: Int = -1
+ override var allowCustom: Boolean = true
+ override var allowCustomAlpha: Boolean = false
+ override var isAccent: Boolean = false
+ override var defaultColor: Int = Color.BLACK
+ override var doneText: Int = R.string.kau_done
+ override var backText: Int = R.string.kau_back
+ override var cancelText: Int = R.string.kau_cancel
+ override var presetText: Int = R.string.kau_md_presets
+ override var customText: Int = R.string.kau_md_custom
+ get() = if (allowCustom) field else 0
+ override var dynamicButtonColors: Boolean = true
+ override var circleSizeRes: Int = R.dimen.kau_color_circle_size
+ override var colorCallback: ((selectedColor: Int) -> Unit)? = null
+ override var colorsTop: IntArray? = null
+ override var colorsSub: Array<IntArray>? = null
+ override var theme: Theme? = null
+}
+
+interface ColorContract {
+ var title: String?
+ var titleRes: Int @StringRes set
+ var allowCustom: Boolean
+ var allowCustomAlpha: Boolean
+ var isAccent: Boolean
+ var defaultColor: Int @StringRes set
+ var doneText: Int @StringRes set
+ var backText: Int @StringRes set
+ var cancelText: Int @StringRes set
+ var presetText: Int
+ @StringRes set
+ var customText: Int @StringRes set
+ var dynamicButtonColors: Boolean
+ var circleSizeRes: Int @DimenRes set
+ var colorCallback: ((selectedColor: Int) -> Unit)?
+ var colorsTop: IntArray?
+ var colorsSub: Array<IntArray>?
+ var theme: Theme?
+}
+
+/**
+ * This is the extension that allows us to initialize the dialog
+ * Note that this returns just the dialog; you still need to call .show() to show it
+ */
+fun Context.colorPickerDialog(action: ColorContract.() -> Unit): MaterialDialog {
+ val b = ColorBuilder()
+ b.action()
+ return colorPickerDialog(b)
+}
+
+fun Context.colorPickerDialog(contract: ColorContract): MaterialDialog {
+ val view = ColorPickerView(this)
+ val dialog = with(MaterialDialog.Builder(this)) {
+ title(string(contract.titleRes, contract.title) ?: string(R.string.kau_md_color_palette))
+ customView(view, false)
+ autoDismiss(false)
+ positiveText(contract.doneText)
+ negativeText(contract.cancelText)
+ if (contract.allowCustom) neutralText(contract.presetText)
+ onPositive { dialog, _ -> contract.colorCallback?.invoke(view.selectedColor); dialog.dismiss() }
+ onNegative { _, _ -> view.backOrCancel() }
+ if (contract.allowCustom) onNeutral { _, _ -> view.toggleCustom() }
+ showListener { view.refreshColors() }
+ if (contract.theme != null) theme(contract.theme!!)
+ build()
+ }
+ view.bind(contract, dialog)
+ return dialog
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerView.kt b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerView.kt
new file mode 100644
index 0000000..da864c9
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerView.kt
@@ -0,0 +1,309 @@
+package ca.allanwang.kau.dialogs.color
+
+import android.content.Context
+import android.graphics.Color
+import android.support.annotation.ColorInt
+import android.support.v4.content.res.ResourcesCompat
+import android.text.Editable
+import android.text.InputFilter
+import android.text.TextWatcher
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.*
+import com.afollestad.materialdialogs.DialogAction
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.color.FillGridView
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ *
+ * ColorPicker component of the ColorPickerDialog
+ */
+internal class ColorPickerView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ScrollView(context, attrs, defStyleAttr) {
+ var selectedColor: Int = -1
+ var isInSub: Boolean = false
+ var isInCustom: Boolean = false
+ var circleSize: Int = context.dimen(R.dimen.kau_color_circle_size).toInt()
+ val backgroundColor = context.resolveColor(R.attr.md_background_color,
+ if (context.resolveColor(android.R.attr.textColorPrimary).isColorDark()) Color.WHITE else 0xff424242.toInt())
+ val backgroundColorTint = backgroundColor.colorToForeground()
+ lateinit var dialog: MaterialDialog
+ lateinit var builder: ColorContract
+ lateinit var colorsTop: IntArray
+ var colorsSub: Array<IntArray>? = null
+ var topIndex: Int = -1
+ var subIndex: Int = -1
+ var colorIndex: Int
+ get() = if (isInSub) subIndex else topIndex
+ set(value) {
+ if (isInSub) subIndex = value
+ else {
+ topIndex = value
+ if (colorsSub != null && colorsSub!!.size > value) {
+ dialog.setActionButton(DialogAction.NEGATIVE, builder.backText)
+ isInSub = true
+ invalidateGrid()
+ }
+ }
+ }
+
+
+ val gridView: FillGridView by bindView(R.id.md_grid)
+ val customFrame: LinearLayout by bindView(R.id.md_colorChooserCustomFrame)
+ val customColorIndicator: View by bindView(R.id.md_colorIndicator)
+ val hexInput: EditText by bindView(R.id.md_hexInput)
+ val alphaLabel: TextView by bindView(R.id.md_colorALabel)
+ val alphaSeekbar: SeekBar by bindView(R.id.md_colorA)
+ val alphaValue: TextView by bindView(R.id.md_colorAValue)
+ val redSeekbar: SeekBar by bindView(R.id.md_colorR)
+ val redValue: TextView by bindView(R.id.md_colorRValue)
+ val greenSeekbar: SeekBar by bindView(R.id.md_colorG)
+ val greenValue: TextView by bindView(R.id.md_colorGValue)
+ val blueSeekbar: SeekBar by bindView(R.id.md_colorB)
+ val blueValue: TextView by bindView(R.id.md_colorBValue)
+
+ var customHexTextWatcher: TextWatcher? = null
+ var customRgbListener: SeekBar.OnSeekBarChangeListener? = null
+
+ init {
+ View.inflate(context, R.layout.md_dialog_colorchooser, this)
+ }
+
+ fun bind(builder: ColorContract, dialog: MaterialDialog) {
+ this.builder = builder
+ this.dialog = dialog
+ this.colorsTop = with(builder) {
+ if (colorsTop != null) colorsTop!!
+ else if (isAccent) ColorPalette.ACCENT_COLORS
+ else ColorPalette.PRIMARY_COLORS
+ }
+ this.colorsSub = with(builder) {
+ if (colorsTop != null) colorsSub
+ else if (isAccent) ColorPalette.ACCENT_COLORS_SUB
+ else ColorPalette.PRIMARY_COLORS_SUB
+ }
+ this.selectedColor = builder.defaultColor
+ if (builder.allowCustom) {
+ if (!builder.allowCustomAlpha) {
+ alphaLabel.gone()
+ alphaSeekbar.gone()
+ alphaValue.gone()
+ hexInput.hint = String.format("%06X", selectedColor)
+ hexInput.filters = arrayOf(InputFilter.LengthFilter(6))
+ } else {
+ hexInput.hint = String.format("%08X", selectedColor)
+ hexInput.filters = arrayOf(InputFilter.LengthFilter(8))
+ }
+ }
+ if (findColor(builder.defaultColor) || !builder.allowCustom) isInCustom = true //when toggled this will be false
+ toggleCustom()
+ }
+
+ fun backOrCancel() {
+ if (isInSub) {
+ dialog.setActionButton(DialogAction.NEGATIVE, builder.cancelText)
+ //to top
+ isInSub = false
+ subIndex = -1
+ invalidateGrid()
+ } else {
+ dialog.cancel()
+ }
+ }
+
+ fun toggleCustom() {
+ isInCustom = !isInCustom
+ if (isInCustom) {
+ isInSub = false
+ if (builder.allowCustom) dialog.setActionButton(DialogAction.NEUTRAL, builder.presetText)
+ dialog.setActionButton(DialogAction.NEGATIVE, builder.cancelText)
+ customHexTextWatcher = object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ try {
+ selectedColor = Color.parseColor("#" + s.toString())
+ } catch (e: IllegalArgumentException) {
+ selectedColor = Color.BLACK
+ }
+
+ customColorIndicator.setBackgroundColor(selectedColor)
+ if (alphaSeekbar.isVisible()) {
+ val alpha = Color.alpha(selectedColor)
+ alphaSeekbar.progress = alpha
+ alphaValue.text = String.format(Locale.CANADA, "%d", alpha)
+ }
+ redSeekbar.progress = Color.red(selectedColor)
+ greenSeekbar.progress = Color.green(selectedColor)
+ blueSeekbar.progress = Color.blue(selectedColor)
+ isInSub = false
+ topIndex = -1
+ subIndex = -1
+ refreshColors()
+ }
+
+ override fun afterTextChanged(s: Editable?) {}
+ }
+ hexInput.setText(selectedColor.toHexString(builder.allowCustomAlpha, false))
+ hexInput.addTextChangedListener(customHexTextWatcher)
+ customRgbListener = object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ val color = if (builder.allowCustomAlpha)
+ Color.argb(alphaSeekbar.progress,
+ redSeekbar.progress,
+ greenSeekbar.progress,
+ blueSeekbar.progress)
+ else Color.rgb(redSeekbar.progress,
+ greenSeekbar.progress,
+ blueSeekbar.progress)
+
+ hexInput.setText(color.toHexString(builder.allowCustomAlpha, false))
+ }
+ if (builder.allowCustomAlpha) alphaValue.text = alphaSeekbar.progress.toString()
+ redValue.text = redSeekbar.progress.toString()
+ greenValue.text = greenSeekbar.progress.toString()
+ blueValue.text = blueSeekbar.progress.toString()
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {}
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {}
+ }
+ redSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ greenSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ blueSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ if (alphaSeekbar.isVisible())
+ alphaSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ hexInput.setText(selectedColor.toHexString(alphaSeekbar.isVisible(), false))
+ gridView.fadeOut(onFinish = { gridView.gone() })
+ customFrame.fadeIn()
+ } else {
+ findColor(selectedColor)
+ if (builder.allowCustom) dialog.setActionButton(DialogAction.NEUTRAL, builder.customText)
+ dialog.setActionButton(DialogAction.NEGATIVE, if (isInSub) builder.backText else builder.cancelText)
+ gridView.fadeIn(onStart = { invalidateGrid() })
+ customFrame.fadeOut(onFinish = { customFrame.gone() })
+ hexInput.removeTextChangedListener(customHexTextWatcher)
+ customHexTextWatcher = null
+ alphaSeekbar.setOnSeekBarChangeListener(null)
+ redSeekbar.setOnSeekBarChangeListener(null)
+ greenSeekbar.setOnSeekBarChangeListener(null)
+ blueSeekbar.setOnSeekBarChangeListener(null)
+ customRgbListener = null
+ }
+ }
+
+ fun refreshColors() {
+ if (!isInCustom) findColor(selectedColor)
+ //Ensure that our tinted color is still visible against the background
+ val visibleColor = if (selectedColor.isColorVisibleOn(backgroundColor)) selectedColor else backgroundColorTint
+ if (builder.dynamicButtonColors) {
+ dialog.getActionButton(DialogAction.POSITIVE).setTextColor(visibleColor)
+ dialog.getActionButton(DialogAction.NEGATIVE).setTextColor(visibleColor)
+ dialog.getActionButton(DialogAction.NEUTRAL).setTextColor(visibleColor)
+ }
+ if (!builder.allowCustom || !isInCustom) return
+ if (builder.allowCustomAlpha)
+ alphaSeekbar.visible().tint(visibleColor)
+ redSeekbar.tint(visibleColor)
+ greenSeekbar.tint(visibleColor)
+ blueSeekbar.tint(visibleColor)
+ hexInput.tint(visibleColor)
+ }
+
+ fun findColor(@ColorInt color: Int): Boolean {
+ topIndex = -1
+ subIndex = -1
+ colorsTop.forEachIndexed {
+ index, topColor ->
+ if (findSubColor(color, index)) {
+ topIndex = index
+ return true
+ }
+ if (topColor == color) { // If no sub colors exists and top color matches
+ topIndex = index
+ return true
+ }
+ }
+ return false
+ }
+
+ fun findSubColor(@ColorInt color: Int, topIndex: Int): Boolean {
+ if (colorsSub == null || colorsSub!!.size <= topIndex) return false
+ colorsSub!![topIndex].forEachIndexed {
+ index, subColor ->
+ if (subColor == color) {
+ subIndex = index
+ return true
+ }
+ }
+ return false
+ }
+
+ fun invalidateGrid() {
+ if (gridView.adapter == null) {
+ gridView.adapter = ColorGridAdapter()
+ gridView.selector = ResourcesCompat.getDrawable(resources, R.drawable.kau_transparent, null)
+ } else {
+ (gridView.adapter as BaseAdapter).notifyDataSetChanged()
+ }
+ }
+
+ inner class ColorGridAdapter : BaseAdapter(), OnClickListener, OnLongClickListener {
+ override fun onClick(v: View) {
+ if (v.tag != null && v.tag is String) {
+ val tags = (v.tag as String).split(":")
+ if (colorIndex == tags[0].toInt()) {
+ colorIndex = tags[0].toInt() //Go to sub list if exists
+ return
+ }
+ if (colorIndex != -1) (gridView.getChildAt(colorIndex) as CircleView).animateSelected(false)
+ selectedColor = tags[1].toInt()
+ refreshColors()
+ val currentSub = isInSub
+ colorIndex = tags[0].toInt()
+ if (currentSub == isInSub) (gridView.getChildAt(colorIndex) as CircleView).animateSelected(true)
+ //Otherwise we are invalidating our grid, so there is no point in animating
+ }
+ }
+
+ override fun onLongClick(v: View): Boolean {
+ if (v.tag != null && v.tag is String) {
+ val tag = (v.tag as String).split(":")
+ val color = tag[1].toInt()
+ (v as CircleView).showHint(color)
+ return true
+ }
+ return false
+ }
+
+ override fun getItem(position: Int): Any = if (isInSub) colorsSub!![topIndex][position] else colorsTop[position]
+
+ override fun getCount(): Int = if (isInSub) colorsSub!![topIndex].size else colorsTop.size
+
+ override fun getItemId(position: Int): Long = position.toLong()
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view: CircleView = if (convertView == null)
+ CircleView(context).apply { layoutParams = AbsListView.LayoutParams(circleSize, circleSize) }
+ else
+ convertView as CircleView
+ val color: Int = if (isInSub) colorsSub!![topIndex][position] else colorsTop[position]
+ return view.apply {
+ setBackgroundColor(color)
+ isSelected = colorIndex == position
+ tag = "$position:$color"
+ setOnClickListener(this@ColorGridAdapter)
+ setOnLongClickListener(this@ColorGridAdapter)
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt b/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt
new file mode 100644
index 0000000..b03a620
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/email/EmailBuilder.kt
@@ -0,0 +1,92 @@
+package ca.allanwang.kau.email
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.support.annotation.StringRes
+import android.util.DisplayMetrics
+import ca.allanwang.kau.R
+import ca.allanwang.kau.logging.KL
+import ca.allanwang.kau.utils.installerPackageName
+import ca.allanwang.kau.utils.isAppInstalled
+import ca.allanwang.kau.utils.string
+
+
+/**
+ * Created by Allan Wang on 2017-06-20.
+ */
+class EmailBuilder(val email: String, val subject: String) {
+ var message: String = "Write here."
+ var deviceDetails: Boolean = true
+ var appInfo: Boolean = true
+ var footer: String? = null
+ private val pairs: MutableMap<String, String> = mutableMapOf()
+ private val packages: MutableList<Package> = mutableListOf()
+
+ fun checkPackage(packageName: String, appName: String) = packages.add(Package(packageName, appName))
+
+ fun addItem(key: String, value: String) = pairs.put(key, value)
+
+ data class Package(val packageName: String, val appName: String)
+
+ fun execute(context: Context) {
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse("mailto:$email"))
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject)
+ val emailBuilder = StringBuilder()
+ emailBuilder.append(message).append("\n\n")
+ if (deviceDetails) {
+ val deviceItems = mutableMapOf(
+ "OS Version" to "${System.getProperty("os.version")} (${Build.VERSION.INCREMENTAL})",
+ "OS API Level" to Build.DEVICE,
+ "Manufacturer" to Build.MANUFACTURER,
+ "Model (and Product)" to "${Build.MODEL} (${Build.PRODUCT})",
+ "Package Installer" to (context.installerPackageName ?: "None")
+ )
+ if (context is Activity) {
+ val metric = DisplayMetrics()
+ context.windowManager.defaultDisplay.getMetrics(metric)
+ deviceItems.put("Screen Dimensions", "${metric.widthPixels} x ${metric.heightPixels}")
+ }
+ deviceItems.forEach { (k, v) -> emailBuilder.append("$k: $v\n") }
+ }
+ if (appInfo) {
+ try {
+ val appInfo = context.packageManager.getPackageInfo(context.packageName, 0)
+ emailBuilder.append("\nApp: ").append(context.packageName)
+ .append("\nApp Version Name: ").append(appInfo.versionName)
+ .append("\nApp Version Code: ").append(appInfo.versionCode).append("\n")
+ } catch (e: PackageManager.NameNotFoundException) {
+ KL.e("EmailBuilder packageInfo not found")
+ }
+ }
+
+ if (packages.isNotEmpty()) emailBuilder.append("\n")
+ packages.forEach {
+ if (context.isAppInstalled(it.packageName))
+ emailBuilder.append(String.format("\n%s is installed", it.appName))
+ }
+
+ if (pairs.isNotEmpty()) emailBuilder.append("\n")
+ pairs.forEach { (k, v) -> emailBuilder.append("$k: $v\n") }
+
+ if (footer != null)
+ emailBuilder.append("\n").append(footer)
+
+ intent.putExtra(Intent.EXTRA_TEXT, emailBuilder.toString())
+ context.startActivity(Intent.createChooser(intent, context.resources.getString(R.string.kau_send_via)))
+ }
+}
+
+fun Context.sendEmail(@StringRes emailId: Int, @StringRes subjectId: Int, builder: EmailBuilder.() -> Unit = {})
+ = sendEmail(string(emailId), string(subjectId), builder)
+
+
+fun Context.sendEmail(email: String, subject: String, builder: EmailBuilder.() -> Unit = {}) {
+ EmailBuilder(email, subject).apply {
+ builder()
+ execute(this@sendEmail)
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt b/core/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt
new file mode 100644
index 0000000..3380ade
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/iitems/CardIItem.kt
@@ -0,0 +1,127 @@
+package ca.allanwang.kau.iitems
+
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.support.v7.widget.CardView
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.widget.Button
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.*
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.items.AbstractItem
+import com.mikepenz.fastadapter.listeners.ClickEventHook
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Simple generic card item with an icon, title, description and button
+ * The icon and button are hidden by default unless values are given
+ */
+class CardIItem(val builder: Config.() -> Unit = {}
+) : AbstractItem<CardIItem, CardIItem.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
+
+ companion object {
+ @JvmStatic fun bindClickEvents(fastAdapter: FastAdapter<IItem<*,*>>) {
+ fastAdapter.withEventHook(object : ClickEventHook<IItem<*,*>>() {
+ override fun onBindMany(viewHolder: RecyclerView.ViewHolder): List<View>? {
+ return if (viewHolder is ViewHolder) listOf(viewHolder.card, viewHolder.button) else null
+ }
+
+ override fun onClick(v: View, position: Int, adapter: FastAdapter<IItem<*,*>>, item: IItem<*,*>) {
+ if (item !is CardIItem) return
+ with(item.configs) {
+ when (v.id) {
+ R.id.kau_card_container -> cardClick?.onClick(v)
+ R.id.kau_card_button -> buttonClick?.onClick(v)
+ else -> {
+ }
+ }
+ }
+ }
+ })
+ }
+ }
+
+ val configs = Config().apply { builder() }
+
+ class Config {
+ var title: String? = null
+ var titleRes: Int = -1
+ var desc: String? = null
+ var descRes: Int = -1
+ var button: String? = null
+ var buttonRes: Int = -1
+ var buttonClick: View.OnClickListener? = null
+ var cardClick: View.OnClickListener? = null
+ var image: Drawable? = null
+ var imageIIcon: IIcon? = null
+ var imageIIconColor: Int = Color.WHITE
+ var imageRes: Int = -1
+ }
+
+
+ override fun getType(): Int = R.id.kau_item_card
+
+ override fun getLayoutRes(): Int = R.layout.kau_iitem_card
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
+ super.bindView(holder, payloads)
+ with(holder.itemView.context) context@ {
+ with(configs) {
+ holder.title.text = string(titleRes, title)
+ holder.description.text = string(descRes, desc)
+ val buttonText = string(buttonRes, button)
+ if (buttonText != null) {
+ holder.bottomRow.visible()
+ holder.button.text = buttonText
+ holder.button.setOnClickListener(buttonClick)
+ }
+ holder.icon.setImageDrawable(
+ if (imageRes > 0) drawable(imageRes)
+ else if (imageIIcon != null) imageIIcon!!.toDrawable(this@context, sizeDp = 40, color = imageIIconColor)
+ else image
+ )
+ holder.card.setOnClickListener(cardClick)
+ }
+ with(holder) {
+ bindTextColor(title)
+ bindTextColorSecondary(description)
+ bindAccentColor(button)
+ if (configs.imageIIcon != null) bindIconColor(icon)
+ bindBackgroundRipple(card)
+ }
+ }
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ icon.gone().setImageDrawable(null)
+ title.text = null
+ description.text = null
+ bottomRow.gone()
+ button.setOnClickListener(null)
+ card.setOnClickListener(null)
+ }
+ }
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val card: CardView by bindView(R.id.kau_card_container)
+ val icon: ImageView by bindView(R.id.kau_card_image)
+ val title: TextView by bindView(R.id.kau_card_title)
+ val description: TextView by bindView(R.id.kau_card_description)
+ val bottomRow: LinearLayout by bindView(R.id.kau_card_bottom_row)
+ val button: Button by bindView(R.id.kau_card_button)
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/iitems/CutoutIItem.kt b/core/src/main/kotlin/ca/allanwang/kau/iitems/CutoutIItem.kt
new file mode 100644
index 0000000..627e1df
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/iitems/CutoutIItem.kt
@@ -0,0 +1,48 @@
+package ca.allanwang.kau.iitems
+
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.views.CutoutView
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Just a cutout item with some defaults in [R.layout.kau_iitem_cutout]
+ */
+class CutoutIItem(val config: CutoutView.() -> Unit = {}
+) : AbstractItem<CutoutIItem, CutoutIItem.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
+
+ override fun getType(): Int = R.id.kau_item_cutout
+
+ override fun getLayoutRes(): Int = R.layout.kau_iitem_cutout
+
+ override fun isSelectable(): Boolean = false
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
+ super.bindView(holder, payloads)
+ with(holder) {
+ if (accentColor != null && themeEnabled) cutout.foregroundColor = accentColor!!
+ cutout.config()
+ }
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ cutout.drawable = null
+ cutout.text = "Text" //back to default
+ }
+ }
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val cutout: CutoutView by bindView(R.id.kau_cutout)
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt b/core/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt
new file mode 100644
index 0000000..e994781
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/iitems/HeaderIItem.kt
@@ -0,0 +1,49 @@
+package ca.allanwang.kau.iitems
+
+import android.support.v7.widget.CardView
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.string
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Simple Header with lots of padding on the top
+ * Contains only one text view
+ */
+class HeaderIItem(text: String? = null, var textRes: Int = -1
+) : AbstractItem<HeaderIItem, HeaderIItem.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
+
+ var text: String = text ?: "Header Placeholder"
+
+ override fun getType(): Int = R.id.kau_item_header_big_margin_top
+
+ override fun getLayoutRes(): Int = R.layout.kau_iitem_header
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
+ super.bindView(holder, payloads)
+ holder.text.text = holder.itemView.context.string(textRes, text)
+ bindTextColor(holder.text)
+ bindBackgroundColor(holder.container)
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ holder.text.text = null
+ }
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val text: TextView by bindView(R.id.kau_header_text)
+ val container: CardView by bindView(R.id.kau_header_container)
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt b/core/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt
new file mode 100644
index 0000000..00b165c
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/iitems/KauIItem.kt
@@ -0,0 +1,23 @@
+package ca.allanwang.kau.iitems
+
+import android.support.annotation.LayoutRes
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import com.mikepenz.fastadapter.IClickable
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * Created by Allan Wang on 2017-07-03.
+ *
+ * Kotlin implementation of the [AbstractItem] to make things shorter
+ */
+open class KauIItem<Item, VH : RecyclerView.ViewHolder>(
+ private val type: Int,
+ @param:LayoutRes private val layoutRes: Int,
+ private val viewHolder: (v: View) -> VH
+) : AbstractItem<Item, VH>() where Item : IItem<*, *>, Item : IClickable<*> {
+ override final fun getType(): Int = type
+ override final fun getViewHolder(v: View): VH = viewHolder(v)
+ override final fun getLayoutRes(): Int = layoutRes
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/iitems/LibraryIItem.kt b/core/src/main/kotlin/ca/allanwang/kau/iitems/LibraryIItem.kt
new file mode 100644
index 0000000..aabd9e3
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/iitems/LibraryIItem.kt
@@ -0,0 +1,100 @@
+package ca.allanwang.kau.iitems
+
+import android.os.Build
+import android.support.v7.widget.CardView
+import android.support.v7.widget.RecyclerView
+import android.text.Html
+import android.view.View
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.gone
+import ca.allanwang.kau.utils.startLink
+import ca.allanwang.kau.utils.visible
+import ca.allanwang.kau.views.createSimpleRippleDrawable
+import com.mikepenz.aboutlibraries.entity.Library
+import com.mikepenz.fastadapter.FastAdapter
+import com.mikepenz.fastadapter.IItem
+import com.mikepenz.fastadapter.items.AbstractItem
+
+/**
+ * Created by Allan Wang on 2017-06-27.
+ */
+class LibraryIItem(val lib: Library
+) : AbstractItem<LibraryIItem, LibraryIItem.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
+
+ companion object {
+ @JvmStatic fun bindClickEvents(fastAdapter: FastAdapter<IItem<*, *>>) {
+ fastAdapter.withOnClickListener { v, _, item, _ ->
+ if (item !is LibraryIItem) false
+ else {
+ val c = v.context
+ with(item.lib) {
+ c.startLink(libraryWebsite, repositoryLink, authorWebsite)
+ }
+ true
+ }
+ }
+ }
+ }
+
+ override fun getType(): Int = R.id.kau_item_library
+
+ override fun getLayoutRes(): Int = R.layout.kau_iitem_library
+
+ override fun isSelectable(): Boolean = false
+
+ override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
+ super.bindView(holder, payloads)
+ with(holder) {
+ name.text = lib.libraryName
+ creator.text = lib.author
+ description.text = if (lib.libraryDescription.isBlank()) lib.libraryDescription
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ Html.fromHtml(lib.libraryDescription, Html.FROM_HTML_MODE_LEGACY)
+ else Html.fromHtml(lib.libraryDescription)
+ bottomDivider.gone()
+ if (lib.libraryVersion?.isNotBlank() ?: false) {
+ bottomDivider.visible()
+ version.visible().text = lib.libraryVersion
+ }
+ if (lib.license?.licenseName?.isNotBlank() ?: false) {
+ bottomDivider.visible()
+ license.visible().text = lib.license?.licenseName
+ }
+ bindTextColor(name, creator)
+ bindTextColorSecondary(description)
+ bindAccentColor(license, version)
+ bindDividerColor(divider, bottomDivider)
+ bindBackgroundRipple(card)
+ }
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ name.text = null
+ creator.text = null
+ description.text = null
+ bottomDivider.gone()
+ version.gone().text = null
+ license.gone().text = null
+ }
+ }
+
+ override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val card: CardView by bindView(R.id.lib_item_card)
+ val name: TextView by bindView(R.id.lib_item_name)
+ val creator: TextView by bindView(R.id.lib_item_author)
+ val description: TextView by bindView(R.id.lib_item_description)
+ val version: TextView by bindView(R.id.lib_item_version)
+ val license: TextView by bindView(R.id.lib_item_license)
+ val divider: View by bindView(R.id.lib_item_top_divider)
+ val bottomDivider: View by bindView(R.id.lib_item_bottom_divider)
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt
new file mode 100644
index 0000000..8b59539
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt
@@ -0,0 +1,50 @@
+package ca.allanwang.kau.kotlin
+
+import android.content.Context
+import android.support.annotation.AnimatorRes
+import android.support.annotation.InterpolatorRes
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.view.animation.Interpolator
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ *
+ * Lazy retrieval of context based items
+ * Items are retrieved using delegateName[context]
+ *
+ */
+
+fun lazyInterpolator(@InterpolatorRes id: Int) = lazyContext<Interpolator> { AnimationUtils.loadInterpolator(it, id) }
+fun lazyAnimation(@AnimatorRes id: Int) = lazyContext<Animation> { AnimationUtils.loadAnimation(it, id) }
+
+fun <T : Any> lazyContext(initializer: (context: Context) -> T): LazyContext<T> = LazyContext<T>(initializer)
+
+class LazyContext<out T : Any>(private val initializer: (context: Context) -> T, lock: Any? = null) {
+ @Volatile private var _value: Any = UNINITIALIZED
+ private val lock = lock ?: this
+
+ fun invalidate() {
+ _value = UNINITIALIZED
+ }
+
+ operator fun invoke(context: Context): T {
+ val _v1 = _value
+ if (_v1 !== UNINITIALIZED)
+ @Suppress("UNCHECKED_CAST")
+ return _v1 as T
+
+ return synchronized(lock) {
+ val _v2 = _value
+ if (_v2 !== UNINITIALIZED) {
+ @Suppress("UNCHECKED_CAST")
+ _v2 as T
+ } else {
+ val typedValue = initializer(context)
+ _value = typedValue
+ typedValue
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
new file mode 100644
index 0000000..f8947f3
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
@@ -0,0 +1,51 @@
+package ca.allanwang.kau.kotlin
+
+import java.io.Serializable
+import kotlin.reflect.KProperty
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ *
+ * Lazy delegate that can be invalidated if needed
+ * https://stackoverflow.com/a/37294840/4407321
+ */
+internal object UNINITIALIZED
+
+fun <T : Any> lazyResettable(initializer: () -> T): LazyResettable<T> = LazyResettable<T>(initializer)
+
+class LazyResettable<T : Any>(private val initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
+ @Volatile private var _value: Any = UNINITIALIZED
+ private val lock = lock ?: this
+
+ fun invalidate() {
+ _value = UNINITIALIZED
+ }
+
+ override val value: T
+ get() {
+ val _v1 = _value
+ if (_v1 !== UNINITIALIZED)
+ @Suppress("UNCHECKED_CAST")
+ return _v1 as T
+
+ return synchronized(lock) {
+ val _v2 = _value
+ if (_v2 !== UNINITIALIZED) {
+ @Suppress("UNCHECKED_CAST")
+ _v2 as T
+ } else {
+ val typedValue = initializer()
+ _value = typedValue
+ typedValue
+ }
+ }
+ }
+
+ override fun isInitialized(): Boolean = _value !== UNINITIALIZED
+
+ override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
+
+ operator fun setValue(any: Any, property: KProperty<*>, t: T) {
+ _value = t
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kotlin/NonReadablePropertyException.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/NonReadablePropertyException.kt
new file mode 100644
index 0000000..f3add48
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/NonReadablePropertyException.kt
@@ -0,0 +1,12 @@
+package ca.allanwang.kau.kotlin
+
+/**
+ * Created by Allan Wang on 2017-06-24.
+ *
+ * Credits to @zsmb13
+ *
+ * https://github.com/zsmb13/MaterialDrawerKt/blob/master/library/src/main/java/co/zsmb/materialdrawerkt/NonReadablePropertyException.kt
+ */
+class NonReadablePropertyException : Exception()
+
+fun nonReadable(): Nothing = throw NonReadablePropertyException() \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt
new file mode 100644
index 0000000..7fd8955
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt
@@ -0,0 +1,35 @@
+package ca.allanwang.kau.kpref
+
+import android.content.Context
+import android.content.SharedPreferences
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+open class KPref {
+
+ lateinit private var c: Context
+ lateinit internal var PREFERENCE_NAME: String
+ private var initialized = false
+
+ fun initialize(c: Context, preferenceName: String) {
+ if (initialized) throw KPrefException("KPref object $preferenceName has already been initialized; please only do so once")
+ initialized = true
+ this.c = c.applicationContext
+ PREFERENCE_NAME = preferenceName
+ }
+
+ internal val sp: SharedPreferences by lazy {
+ if (!initialized) throw KPrefException("KPref object has not yet been initialized; please initialize it with a context and preference name")
+ c.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
+ }
+
+ internal val prefMap: MutableMap<String, KPrefDelegate<*>> = mutableMapOf()
+
+ fun reset() {
+ prefMap.values.forEach { it.invalidate() }
+ }
+
+ operator fun get(key: String): KPrefDelegate<*>? = prefMap[key]
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt
new file mode 100644
index 0000000..9a9f7d4
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt
@@ -0,0 +1,137 @@
+package ca.allanwang.kau.kpref
+
+import android.os.Bundle
+import android.support.annotation.StringRes
+import android.support.constraint.ConstraintLayout
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.widget.RecyclerView
+import android.support.v7.widget.Toolbar
+import android.view.View
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.widget.FrameLayout
+import android.widget.ViewAnimator
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.items.KPrefItemCore
+import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.resolveColor
+import ca.allanwang.kau.utils.statusBarColor
+import ca.allanwang.kau.utils.string
+import ca.allanwang.kau.widgets.TextSlider
+import ca.allanwang.kau.views.RippleCanvas
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+
+abstract class KPrefActivity : AppCompatActivity(), KPrefActivityContract {
+
+ val adapter: FastItemAdapter<KPrefItemCore>
+ @Suppress("UNCHECKED_CAST")
+ get() = recycler.adapter as FastItemAdapter<KPrefItemCore>
+ val recycler: RecyclerView
+ get() = prefHolder.currentView as RecyclerView
+ val container: ConstraintLayout by bindView(R.id.kau_container)
+ val bgCanvas: RippleCanvas by bindView(R.id.kau_ripple)
+ val toolbarCanvas: RippleCanvas by bindView(R.id.kau_toolbar_ripple)
+ val toolbar: Toolbar by bindView(R.id.kau_toolbar)
+ val toolbarTitle: TextSlider by bindView(R.id.kau_toolbar_text)
+ val prefHolder: ViewAnimator by bindView(R.id.kau_holder)
+ private lateinit var globalOptions: GlobalOptions
+ var animate: Boolean = true
+ set(value) {
+ field = value
+ toolbarTitle.animationType = if (value) TextSlider.ANIMATION_SLIDE_HORIZONTAL else TextSlider.ANIMATION_NONE
+ }
+
+ private val SLIDE_IN_LEFT_ITEMS: Animation by lazy { AnimationUtils.loadAnimation(this, R.anim.kau_slide_in_left) }
+ private val SLIDE_IN_RIGHT_ITEMS: Animation by lazy { AnimationUtils.loadAnimation(this, R.anim.kau_slide_in_right) }
+ private val SLIDE_OUT_LEFT_ITEMS: Animation by lazy { AnimationUtils.loadAnimation(this, R.anim.kau_slide_out_left) }
+ private val SLIDE_OUT_RIGHT_ITEMS: Animation by lazy { AnimationUtils.loadAnimation(this, R.anim.kau_slide_out_right) }
+
+ /**
+ * Core attribute builder that is consistent throughout all items
+ * Leave blank to use defaults
+ */
+ abstract fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ //setup layout
+ setContentView(R.layout.kau_activity_kpref)
+ setSupportActionBar(toolbar)
+ if (supportActionBar != null)
+ with(supportActionBar!!) {
+ setDisplayHomeAsUpEnabled(true)
+ setDisplayShowHomeEnabled(true)
+ toolbar.setNavigationOnClickListener { onBackPressed() }
+ setDisplayShowTitleEnabled(false)
+ }
+ window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ statusBarColor = 0x30000000
+ toolbarCanvas.set(resolveColor(R.attr.colorPrimary))
+ bgCanvas.set(resolveColor(android.R.attr.colorBackground))
+ prefHolder.animateFirstView = false
+ //setup prefs
+ val core = CoreAttributeBuilder()
+ val builder = kPrefCoreAttributes()
+ core.builder()
+ globalOptions = GlobalOptions(core, this)
+ showNextPrefs(R.string.kau_settings, onCreateKPrefs(savedInstanceState))
+ }
+
+ override fun onPostCreate(savedInstanceState: Bundle?) {
+ super.onPostCreate(savedInstanceState)
+ }
+
+ override fun showNextPrefs(@StringRes toolbarTitleRes: Int, builder: KPrefAdapterBuilder.() -> Unit) {
+ val rv = RecyclerView(this).apply {
+ layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
+ setKPrefAdapter(globalOptions, builder)
+ }
+ with(prefHolder) {
+ inAnimation = if (animate) SLIDE_IN_RIGHT_ITEMS else null
+ outAnimation = if (animate) SLIDE_OUT_LEFT_ITEMS else null
+ addView(rv)
+ showNext()
+ }
+ toolbarTitle.setNextText(string(toolbarTitleRes))
+ }
+
+ override fun showPrevPrefs() {
+ val current = prefHolder.currentView
+ with(prefHolder) {
+ inAnimation = if (animate) SLIDE_IN_LEFT_ITEMS else null
+ outAnimation = if (animate) SLIDE_OUT_RIGHT_ITEMS else null
+ showPrevious()
+ removeView(current)
+ adapter.notifyAdapterDataSetChanged()
+ }
+ toolbarTitle.setPrevText()
+ }
+
+ fun reload(vararg index: Int) {
+ if (index.isEmpty()) adapter.notifyAdapterDataSetChanged()
+ else index.forEach { adapter.notifyItemChanged(it) }
+ }
+
+ override fun reloadByTitle(@StringRes vararg title: Int) {
+ if (title.isEmpty()) return
+ adapter.adapterItems.forEachIndexed { index, item ->
+ if (title.any { item.core.titleRes == it })
+ adapter.notifyItemChanged(index)
+ }
+ }
+
+ abstract fun onCreateKPrefs(savedInstanceState: Bundle?): KPrefAdapterBuilder.() -> Unit
+
+ override fun onBackPressed() {
+ if (!backPress()) super.onBackPressed()
+ }
+
+ fun backPress(): Boolean {
+ if (!toolbarTitle.isRoot) {
+ showPrevPrefs()
+ return true
+ }
+ return false
+ }
+}
+
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
new file mode 100644
index 0000000..7f42d2a
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
@@ -0,0 +1,120 @@
+package ca.allanwang.kau.kpref
+
+import android.support.annotation.StringRes
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.items.*
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.uiThread
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ *
+ * Houses all the components that can be called externally to setup the kpref mainAdapter
+ */
+
+/**
+ * Base extension that will register the layout manager and mainAdapter with the given items
+ * Returns FastAdapter
+ */
+fun RecyclerView.setKPrefAdapter(globalOptions: GlobalOptions, builder: KPrefAdapterBuilder.() -> Unit): FastItemAdapter<KPrefItemCore> {
+ layoutManager = LinearLayoutManager(context)
+ val adapter = FastItemAdapter<KPrefItemCore>()
+ adapter.withOnClickListener { v, _, item, _ -> item.onClick(v, v.findViewById(R.id.kau_pref_inner_content)) }
+ this.adapter = adapter
+ doAsync {
+ val items = KPrefAdapterBuilder(globalOptions)
+ builder.invoke(items)
+ uiThread {
+ adapter.add(items.list)
+ }
+ }
+ return adapter
+}
+
+@DslMarker
+annotation class KPrefMarker
+
+/**
+ * Contains attributes shared amongst all kpref items
+ */
+@KPrefMarker
+interface CoreAttributeContract {
+ var textColor: (() -> Int)?
+ var accentColor: (() -> Int)?
+}
+
+/**
+ * Implementation of [CoreAttributeContract]
+ */
+class CoreAttributeBuilder : CoreAttributeContract {
+ override var textColor: (() -> Int)? = null
+ override var accentColor: (() -> Int)? = textColor
+}
+
+interface KPrefActivityContract {
+ fun showNextPrefs(@StringRes toolbarTitleRes: Int, builder: KPrefAdapterBuilder.() -> Unit)
+ fun showPrevPrefs()
+ fun reloadByTitle(@StringRes vararg title: Int)
+}
+
+
+class GlobalOptions(core: CoreAttributeContract, activity: KPrefActivityContract
+) : CoreAttributeContract by core, KPrefActivityContract by activity
+
+
+/**
+ * Builder for kpref items
+ * Contains DSLs for every possible item
+ * The arguments are all the mandatory values plus an optional builder housing all the possible configurations
+ * The mandatory values are final so they cannot be edited in the builder
+ */
+@KPrefMarker
+class KPrefAdapterBuilder(internal val globalOptions: GlobalOptions) {
+
+ @KPrefMarker
+ fun header(@StringRes title: Int)
+ = list.add(KPrefHeader(KPrefItemCore.CoreBuilder(globalOptions, title)))
+
+ @KPrefMarker
+ fun checkbox(@StringRes title: Int,
+ getter: (() -> Boolean),
+ setter: ((value: Boolean) -> Unit),
+ builder: KPrefItemBase.BaseContract<Boolean>.() -> Unit = {})
+ = list.add(KPrefCheckbox(KPrefItemBase.BaseBuilder(globalOptions, title, getter, setter)
+ .apply { builder() }))
+
+ @KPrefMarker
+ fun colorPicker(@StringRes title: Int,
+ getter: (() -> Int),
+ setter: ((value: Int) -> Unit),
+ builder: KPrefColorPicker.KPrefColorContract.() -> Unit = {})
+ = list.add(KPrefColorPicker(KPrefColorPicker.KPrefColorBuilder(globalOptions, title, getter, setter)
+ .apply { builder() }))
+
+ @KPrefMarker
+ fun <T> text(@StringRes title: Int,
+ getter: (() -> T),
+ setter: ((value: T) -> Unit),
+ builder: KPrefText.KPrefTextContract<T>.() -> Unit = {})
+ = list.add(KPrefText<T>(KPrefText.KPrefTextBuilder<T>(globalOptions, title, getter, setter)
+ .apply { builder() }))
+
+ @KPrefMarker
+ fun subItems(@StringRes title: Int,
+ itemBuilder: KPrefAdapterBuilder.() -> Unit,
+ builder: KPrefSubItems.KPrefSubItemsContract.() -> Unit)
+ = list.add(KPrefSubItems(KPrefSubItems.KPrefSubItemsBuilder(globalOptions, title, itemBuilder)
+ .apply { builder() }))
+
+ @KPrefMarker
+ fun plainText(@StringRes title: Int,
+ builder: KPrefItemBase.BaseContract<Unit>.() -> Unit = {})
+ = list.add(KPrefPlainText(KPrefPlainText.KPrefPlainTextBuilder(globalOptions, title)
+ .apply { builder() }))
+
+ @KPrefMarker
+ internal val list: MutableList<KPrefItemCore> = mutableListOf()
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt
new file mode 100644
index 0000000..4d57ff1
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt
@@ -0,0 +1,89 @@
+package ca.allanwang.kau.kpref
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+private object UNINITIALIZED
+
+fun KPref.kpref(key: String, fallback: Boolean, postSetter: (value: Boolean) -> Unit = {}): KPrefDelegate<Boolean> = KPrefDelegate(key, fallback, this, postSetter)
+fun KPref.kpref(key: String, fallback: Double, postSetter: (value: Float) -> Unit = {}): KPrefDelegate<Float> = KPrefDelegate(key, fallback.toFloat(), this, postSetter)
+fun KPref.kpref(key: String, fallback: Float, postSetter: (value: Float) -> Unit = {}): KPrefDelegate<Float> = KPrefDelegate(key, fallback, this, postSetter)
+fun KPref.kpref(key: String, fallback: Int, postSetter: (value: Int) -> Unit = {}): KPrefDelegate<Int> = KPrefDelegate(key, fallback, this, postSetter)
+fun KPref.kpref(key: String, fallback: Long, postSetter: (value: Long) -> Unit = {}): KPrefDelegate<Long> = KPrefDelegate(key, fallback, this, postSetter)
+fun KPref.kpref(key: String, fallback: Set<String>, postSetter: (value: Set<String>) -> Unit = {}): KPrefDelegate<StringSet> = KPrefDelegate(key, StringSet(fallback), this, postSetter)
+fun KPref.kpref(key: String, fallback: String, postSetter: (value: String) -> Unit = {}): KPrefDelegate<String> = KPrefDelegate(key, fallback, this, postSetter)
+
+class StringSet(set: Collection<String>) : LinkedHashSet<String>(set)
+
+/**
+ * Implementation of a kpref data item
+ * Contains a unique key for the shared preference as well as a nonnull fallback item
+ * Also contains an optional mutable postSetter that will be called every time a new value is given
+ */
+class KPrefDelegate<T : Any> internal constructor(private val key: String, private val fallback: T, private val pref: KPref, var postSetter: (value: T) -> Unit = {}, lock: Any? = null) : Lazy<T>, java.io.Serializable {
+
+ @Volatile private var _value: Any = UNINITIALIZED
+ private val lock = lock ?: this
+
+ init {
+ if (pref.prefMap.containsKey(key))
+ throw KPrefException("$key is already used elsewhere in preference ${pref.PREFERENCE_NAME}")
+ pref.prefMap.put(key, this@KPrefDelegate)
+ }
+
+ fun invalidate() {
+ _value = UNINITIALIZED
+ }
+
+ override val value: T
+ get() {
+ val _v1 = _value
+ if (_v1 !== UNINITIALIZED)
+ @Suppress("UNCHECKED_CAST")
+ return _v1 as T
+
+ return synchronized(lock) {
+ val _v2 = _value
+ if (_v2 !== UNINITIALIZED) {
+ @Suppress("UNCHECKED_CAST")
+ _v2 as T
+ } else {
+ _value = when (fallback) {
+ is Boolean -> pref.sp.getBoolean(key, fallback)
+ is Float -> pref.sp.getFloat(key, fallback)
+ is Int -> pref.sp.getInt(key, fallback)
+ is Long -> pref.sp.getLong(key, fallback)
+ is StringSet -> StringSet(pref.sp.getStringSet(key, fallback))
+ is String -> pref.sp.getString(key, fallback)
+ else -> throw KPrefException(fallback)
+ }
+ @Suppress("UNCHECKED_CAST")
+ _value as T
+ }
+ }
+ }
+
+ override fun isInitialized(): Boolean = _value !== UNINITIALIZED
+
+ override fun toString(): String = if (isInitialized()) value.toString() else "Lazy kPref $key not initialized yet."
+
+ operator fun setValue(any: Any, property: kotlin.reflect.KProperty<*>, t: T) {
+ _value = t
+ val editor = pref.sp.edit()
+ when (t) {
+ is Boolean -> editor.putBoolean(key, t)
+ is Float -> editor.putFloat(key, t)
+ is Int -> editor.putInt(key, t)
+ is Long -> editor.putLong(key, t)
+ is StringSet -> editor.putStringSet(key, t)
+ is String -> editor.putString(key, t)
+ else -> throw KPrefException(t)
+ }
+ editor.apply()
+ postSetter.invoke(t)
+ }
+}
+
+class KPrefException(message: String) : IllegalAccessException(message) {
+ constructor(element: Any?) : this("Invalid type in pref cache: ${element?.javaClass?.simpleName ?: "null"}")
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt
new file mode 100644
index 0000000..22cc927
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt
@@ -0,0 +1,33 @@
+package ca.allanwang.kau.kpref.items
+
+import android.view.View
+import android.widget.CheckBox
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.KPrefMarker
+import ca.allanwang.kau.utils.tint
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ *
+ * Checkbox preference
+ * When clicked, will toggle the preference and the apply the result to the checkbox
+ */
+class KPrefCheckbox(builder: BaseContract<Boolean>) : KPrefItemBase<Boolean>(builder) {
+
+ override fun defaultOnClick(itemView: View, innerContent: View?): Boolean {
+ pref = !pref
+ (innerContent as CheckBox).isChecked = pref
+ return true
+ }
+
+ override fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?) {
+ super.onPostBindView(viewHolder, textColor, accentColor)
+ val checkbox = viewHolder.bindInnerView<CheckBox>(R.layout.kau_preference_checkbox)
+ if (accentColor != null) checkbox.tint(accentColor)
+ checkbox.isChecked = pref
+ checkbox.jumpDrawablesToCurrentState() //Cancel the animation
+ }
+
+ override fun getType(): Int = R.id.kau_item_pref_checkbox
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt
new file mode 100644
index 0000000..c573939
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt
@@ -0,0 +1,73 @@
+package ca.allanwang.kau.kpref.items
+
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.dialogs.color.CircleView
+import ca.allanwang.kau.dialogs.color.ColorBuilder
+import ca.allanwang.kau.dialogs.color.ColorContract
+import ca.allanwang.kau.dialogs.color.colorPickerDialog
+import ca.allanwang.kau.kpref.CoreAttributeContract
+import ca.allanwang.kau.kpref.GlobalOptions
+import ca.allanwang.kau.kpref.KPrefMarker
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ *
+ * ColorPicker preference
+ * When a color is successfully selected in the dialog, it will be saved as an int
+ */
+class KPrefColorPicker(val builder: KPrefColorContract) : KPrefItemBase<Int>(builder) {
+
+ override fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?) {
+ super.onPostBindView(viewHolder, textColor, accentColor)
+ builder.apply {
+ titleRes = core.titleRes
+ colorCallback = {
+ pref = it
+ }
+ }
+ if (builder.showPreview) {
+ val preview = viewHolder.bindInnerView<CircleView>(R.layout.kau_preference_color_preview)
+ preview.setBackgroundColor(pref)
+ preview.withBorder = true
+ builder.apply {
+ colorCallback = {
+ pref = it
+ if (builder.showPreview)
+ preview.setBackgroundColor(it)
+ }
+ }
+ }
+ }
+
+
+ override fun defaultOnClick(itemView: View, innerContent: View?): Boolean {
+ builder.apply {
+ defaultColor = pref //update color
+ }
+ itemView.context.colorPickerDialog(builder).show()
+ return true
+ }
+
+ /**
+ * Extension of the base contract and [ColorContract] along with a showPreview option
+ */
+ interface KPrefColorContract : BaseContract<Int>, ColorContract {
+ var showPreview: Boolean
+ }
+
+ /**
+ * Default implementation of [KPrefColorContract]
+ */
+ class KPrefColorBuilder(globalOptions: GlobalOptions,
+ override var titleRes: Int,
+ getter: () -> Int,
+ setter: (value: Int) -> Unit
+ ) : KPrefColorContract, BaseContract<Int> by BaseBuilder<Int>(globalOptions, titleRes, getter, setter),
+ ColorContract by ColorBuilder() {
+ override var showPreview: Boolean = true
+ }
+
+ override fun getType(): Int = R.id.kau_item_pref_color_picker
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt
new file mode 100644
index 0000000..fa8efff
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt
@@ -0,0 +1,25 @@
+package ca.allanwang.kau.kpref.items
+
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.KPrefMarker
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ *
+ * Header preference
+ * This view just holds a title and is not clickable. It is styled using the accent color
+ */
+class KPrefHeader(builder: CoreContract) : KPrefItemCore(builder) {
+
+ override fun getLayoutRes(): Int = R.layout.kau_preference_header
+
+ override fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?) {
+ if (accentColor != null) viewHolder.title.setTextColor(accentColor)
+ }
+
+ override fun onClick(itemView: View, innerContent: View?): Boolean = true
+
+ override fun getType() = R.id.kau_item_pref_header
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt
new file mode 100644
index 0000000..bb0f0a3
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt
@@ -0,0 +1,85 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.CallSuper
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.CoreAttributeContract
+import ca.allanwang.kau.kpref.GlobalOptions
+import ca.allanwang.kau.utils.resolveDrawable
+
+/**
+ * Created by Allan Wang on 2017-06-05.
+ *
+ * Base class for pref setters that include the Shared Preference hooks
+ */
+abstract class KPrefItemBase<T>(val base: BaseContract<T>) : KPrefItemCore(base) {
+
+ open var pref: T
+ get() = base.getter.invoke()
+ set(value) {
+ base.setter.invoke(value)
+ }
+
+ var enabled: Boolean = true
+
+ init {
+ if (base.onClick == null) base.onClick = {
+ itemView, innerContent, _ ->
+ defaultOnClick(itemView, innerContent)
+ }
+ }
+
+ abstract fun defaultOnClick(itemView: View, innerContent: View?): Boolean
+
+ @CallSuper
+ override fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?) {
+ enabled = base.enabler.invoke()
+ with(viewHolder) {
+ if (!enabled) container?.background = null
+ container?.alpha = if (enabled) 1.0f else 0.3f
+ }
+ }
+
+ override final fun onClick(itemView: View, innerContent: View?): Boolean {
+ return if (enabled) base.onClick?.invoke(itemView, innerContent, this) ?: false
+ else base.onDisabledClick?.invoke(itemView, innerContent, this) ?: false
+ }
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ container?.isEnabled = true
+ container?.background = itemView.context.resolveDrawable(android.R.attr.selectableItemBackground)
+ container?.alpha = 1.0f
+ }
+ }
+
+ override final fun getLayoutRes(): Int = R.layout.kau_preference
+
+ /**
+ * Extension of the core contract
+ * Since everything that extends the base is an actual preference, there must be a getter and setter
+ * The rest are optional and will have their defaults
+ */
+ interface BaseContract<T> : CoreContract {
+ var enabler: () -> Boolean
+ var onClick: ((itemView: View, innerContent: View?, item: KPrefItemBase<T>) -> Boolean)?
+ var onDisabledClick: ((itemView: View, innerContent: View?, item: KPrefItemBase<T>) -> Boolean)?
+ val getter: () -> T
+ val setter: (value: T) -> Unit
+ }
+
+ /**
+ * Default implementation of [BaseContract]
+ */
+ class BaseBuilder<T>(globalOptions: GlobalOptions,
+ titleRes: Int,
+ override val getter: () -> T,
+ override val setter: (value: T) -> Unit
+ ) : CoreContract by CoreBuilder(globalOptions, titleRes), BaseContract<T> {
+ override var enabler: () -> Boolean = { true }
+ override var onClick: ((itemView: View, innerContent: View?, item: KPrefItemBase<T>) -> Boolean)? = null
+ override var onDisabledClick: ((itemView: View, innerContent: View?, item: KPrefItemBase<T>) -> Boolean)? = null
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt
new file mode 100644
index 0000000..5f684ba
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt
@@ -0,0 +1,126 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.CallSuper
+import android.support.annotation.IdRes
+import android.support.annotation.LayoutRes
+import android.support.annotation.StringRes
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.adapters.ThemableIItem
+import ca.allanwang.kau.adapters.ThemableIItemDelegate
+import ca.allanwang.kau.kpref.GlobalOptions
+import ca.allanwang.kau.kpref.KPrefMarker
+import ca.allanwang.kau.utils.*
+import com.mikepenz.fastadapter.items.AbstractItem
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-05.
+ *
+ * Core class containing nothing but the view items
+ */
+
+abstract class KPrefItemCore(val core: CoreContract) : AbstractItem<KPrefItemCore, KPrefItemCore.ViewHolder>(),
+ ThemableIItem by ThemableIItemDelegate() {
+
+ override final fun getViewHolder(v: View) = ViewHolder(v)
+
+ @CallSuper
+ override fun bindView(viewHolder: ViewHolder, payloads: List<Any>) {
+ super.bindView(viewHolder, payloads)
+ with(viewHolder) {
+ val context = itemView.context
+ title.text = context.string(core.titleRes)
+ if (core.descRes > 0)
+ desc?.visible()?.setText(core.descRes)
+ else
+ desc?.gone()
+ if (core.iicon != null) icon?.visible()?.setIcon(core.iicon, 24)
+ else icon?.gone()
+ innerFrame?.removeAllViews()
+ val textColor = core.globalOptions.textColor?.invoke()
+ if (textColor != null) {
+ title.setTextColor(textColor)
+ desc?.setTextColor(textColor)
+ }
+ val accentColor = core.globalOptions.accentColor?.invoke()
+ if (accentColor != null) {
+ icon?.drawable?.setTint(accentColor)
+ }
+ onPostBindView(this, textColor, accentColor)
+ }
+ }
+
+ abstract fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?)
+
+ abstract fun onClick(itemView: View, innerContent: View?): Boolean
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ title.text = null
+ desc?.text = null
+ icon?.setImageDrawable(null)
+// innerFrame?.removeAllViews()
+ }
+ }
+
+ /**
+ * Core values for all kpref items
+ */
+ @KPrefMarker
+ interface CoreContract {
+ val globalOptions: GlobalOptions
+ @get:StringRes val titleRes: Int
+ var descRes: Int
+ @StringRes get
+ var iicon: IIcon?
+
+ /**
+ * Attempts to reload current item by identifying it with its [titleRes]
+ */
+ fun reloadSelf()
+ }
+
+ /**
+ * Default implementation of [CoreContract]
+ */
+ class CoreBuilder(override val globalOptions: GlobalOptions,
+ override @param:StringRes val titleRes: Int) : CoreContract {
+ override var descRes: Int = -1
+ override var iicon: IIcon? = null
+
+ override fun reloadSelf() {
+ globalOptions.reloadByTitle(titleRes)
+ }
+ }
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val title: TextView by bindView(R.id.kau_pref_title)
+ val container: ViewGroup? by bindOptionalView(R.id.kau_pref_container)
+ val desc: TextView? by bindOptionalView(R.id.kau_pref_desc)
+ val icon: ImageView? by bindOptionalView(R.id.kau_pref_icon)
+ val innerFrame: LinearLayout? by bindOptionalView(R.id.kau_pref_inner_frame)
+ val innerContent: View?
+ get() = itemView.findViewById(R.id.kau_pref_inner_content)
+
+ inline fun <reified T : View> bindInnerView(@LayoutRes id: Int): T {
+ if (innerFrame == null) throw IllegalStateException("Cannot bind inner view when innerFrame does not exist")
+ if (innerContent !is T) {
+ innerFrame!!.removeAllViews()
+ LayoutInflater.from(innerFrame!!.context).inflate(id, innerFrame)
+ }
+ return innerContent as T
+ }
+
+ inline fun <reified T : View> getInnerView() = innerContent as T
+
+ operator fun get(@IdRes id: Int): View = itemView.findViewById(id)
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt
new file mode 100644
index 0000000..a782430
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefPlainText.kt
@@ -0,0 +1,29 @@
+package ca.allanwang.kau.kpref.items
+
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.GlobalOptions
+
+/**
+ * Created by Allan Wang on 2017-06-14.
+ *
+ * Just text with the core options. Extends base preference but has an empty getter and setter
+ * Useful replacement of [KPrefText] when nothing is displayed on the right side,
+ * and when the preference is completely handled by the click
+ *
+ */
+class KPrefPlainText(val builder: KPrefPlainTextBuilder) : KPrefItemBase<Unit>(builder) {
+
+ override fun defaultOnClick(itemView: View, innerContent: View?): Boolean {
+ //nothing
+ return true
+ }
+
+ class KPrefPlainTextBuilder(
+ globalOptions: GlobalOptions,
+ titleRes: Int
+ ) : BaseContract<Unit> by BaseBuilder(globalOptions, titleRes, {}, {})
+
+ override fun getType(): Int = R.id.kau_item_pref_plain_text
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefSubItems.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefSubItems.kt
new file mode 100644
index 0000000..51625ab
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefSubItems.kt
@@ -0,0 +1,43 @@
+package ca.allanwang.kau.kpref.items
+
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.GlobalOptions
+import ca.allanwang.kau.kpref.KPrefAdapterBuilder
+
+/**
+ * Created by Allan Wang on 2017-06-14.
+ *
+ * Sub item preference
+ * When clicked, will navigate to a new set of preferences and add the old list to a stack
+ *
+ */
+class KPrefSubItems(val builder: KPrefSubItemsContract) : KPrefItemCore(builder) {
+
+ override fun onClick(itemView: View, innerContent: View?): Boolean {
+ builder.globalOptions.showNextPrefs(builder.titleRes, builder.itemBuilder)
+ return true
+ }
+
+ override fun getLayoutRes(): Int = R.layout.kau_preference
+
+ override fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?) {}
+ /**
+ * Extension of the base contract with an optional text getter
+ */
+ interface KPrefSubItemsContract : CoreContract {
+ val itemBuilder: KPrefAdapterBuilder.() -> Unit
+ }
+
+ /**
+ * Default implementation of [KPrefTextContract]
+ */
+ class KPrefSubItemsBuilder(
+ globalOptions: GlobalOptions,
+ titleRes: Int,
+ override val itemBuilder: KPrefAdapterBuilder.() -> Unit
+ ) : KPrefSubItemsContract, CoreContract by CoreBuilder(globalOptions, titleRes)
+
+ override fun getType(): Int = R.id.kau_item_pref_sub_item
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefText.kt b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefText.kt
new file mode 100644
index 0000000..8662b6a
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefText.kt
@@ -0,0 +1,62 @@
+package ca.allanwang.kau.kpref.items
+
+import android.view.View
+import android.widget.TextView
+import ca.allanwang.kau.R
+import ca.allanwang.kau.kpref.GlobalOptions
+import ca.allanwang.kau.utils.toast
+
+/**
+ * Created by Allan Wang on 2017-06-14.
+ *
+ * Text preference
+ * Holds a textview to display data on the right
+ * This is still a generic preference
+ *
+ */
+class KPrefText<T>(val builder: KPrefTextContract<T>) : KPrefItemBase<T>(builder) {
+
+ /**
+ * Automatically reload on set
+ */
+ override var pref: T
+ get() = base.getter.invoke()
+ set(value) {
+ base.setter.invoke(value)
+ builder.reloadSelf()
+ }
+
+ override fun defaultOnClick(itemView: View, innerContent: View?): Boolean {
+ itemView.context.toast("No click function set")
+ return true
+ }
+
+ override fun onPostBindView(viewHolder: ViewHolder, textColor: Int?, accentColor: Int?) {
+ super.onPostBindView(viewHolder, textColor, accentColor)
+ val textview = viewHolder.bindInnerView<TextView>(R.layout.kau_preference_text)
+ if (textColor != null) textview.setTextColor(textColor)
+ textview.text = builder.textGetter.invoke(pref)
+ }
+
+ /**
+ * Extension of the base contract with an optional text getter
+ */
+ interface KPrefTextContract<T> : BaseContract<T> {
+ var textGetter: (T) -> String?
+ }
+
+ /**
+ * Default implementation of [KPrefTextContract]
+ */
+ class KPrefTextBuilder<T>(
+ globalOptions: GlobalOptions,
+ titleRes: Int,
+ getter: () -> T,
+ setter: (value: T) -> Unit
+ ) : KPrefTextContract<T>, BaseContract<T> by BaseBuilder<T>(globalOptions, titleRes, getter, setter) {
+ override var textGetter: (T) -> String? = { it?.toString() }
+ }
+
+ override fun getType(): Int = R.id.kau_item_pref_text
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt b/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt
new file mode 100644
index 0000000..4fa3360
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/logging/KL.kt
@@ -0,0 +1,6 @@
+package ca.allanwang.kau.logging
+
+/**
+ * Created by Allan Wang on 2017-06-19.
+ */
+object KL : TimberLogger("KAU") \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt b/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
new file mode 100644
index 0000000..5969fd5
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
@@ -0,0 +1,19 @@
+package ca.allanwang.kau.logging
+
+import timber.log.Timber
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ *
+ * Timber extension that will embed the tag as part of the message for each log item
+ */
+open class TimberLogger(tag: String) {
+ internal val TAG = "$tag: %s"
+ fun e(s: String) = Timber.e(TAG, s)
+ fun e(t: Throwable, s: String = "error") = Timber.e(t, TAG, s)
+ fun d(s: String) = Timber.d(TAG, s)
+ fun i(s: String) = Timber.i(TAG, s)
+ fun v(s: String) = Timber.v(TAG, s)
+ fun eThrow(s: String) = e(Throwable(s))
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionManager.kt b/core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionManager.kt
new file mode 100644
index 0000000..6f93c9f
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionManager.kt
@@ -0,0 +1,58 @@
+package ca.allanwang.kau.permissions
+
+import android.app.Activity
+import android.content.Context
+import android.support.v4.app.ActivityCompat
+import ca.allanwang.kau.logging.KL
+import ca.allanwang.kau.utils.KauException
+import ca.allanwang.kau.utils.buildIsMarshmallowAndUp
+import ca.allanwang.kau.utils.hasPermission
+import java.lang.ref.WeakReference
+
+/**
+ * Created by Allan Wang on 2017-07-03.
+ */
+internal object PermissionManager {
+
+ var requestInProgress = false
+ val pendingResults: MutableList<WeakReference<PermissionResult>> by lazy { mutableListOf<WeakReference<PermissionResult>>() }
+
+ operator fun invoke(context: Context, permissions: Array<out String>, callback: (granted: Boolean, deniedPerm: String?) -> Unit) {
+ KL.d("Requesting permissions: ${permissions.contentToString()}")
+ if (!buildIsMarshmallowAndUp) return callback(true, null)
+ val missingPermissions = permissions.filter { !context.hasPermission(it) }
+ if (missingPermissions.isEmpty()) return callback(true, null)
+ pendingResults.add(WeakReference(PermissionResult(permissions, callback = callback)))
+ if (!requestInProgress) {
+ requestInProgress = true
+ requestPermissions(context, missingPermissions.toTypedArray())
+ } else KL.d("Request is postponed since another one is still in progress; did you remember to override onRequestPermissionsResult?")
+ }
+
+ @Synchronized internal fun requestPermissions(context: Context, permissions: Array<out String>) {
+ val activity = (context as? Activity) ?: throw KauException("Context is not an instance of an activity; cannot request permissions")
+ ActivityCompat.requestPermissions(activity, permissions, 1)
+ }
+
+ fun onRequestPermissionsResult(context: Context, permissions: Array<out String>, grantResults: IntArray) {
+ val count = Math.min(permissions.size, grantResults.size)
+ val iter = pendingResults.iterator()
+ while (iter.hasNext()) {
+ val action = iter.next().get()
+ if ((0 until count).any { action?.onResult(permissions[it], grantResults[it]) ?: true })
+ iter.remove()
+ }
+ if (pendingResults.isEmpty())
+ requestInProgress = false
+ else {
+ val action = pendingResults.map { it.get() }.firstOrNull { it != null }
+ if (action == null) { //actions have been unlinked from their weak references
+ pendingResults.clear()
+ requestInProgress = false
+ return
+ }
+ requestPermissions(context, action.permissions.toTypedArray())
+ }
+ }
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionResult.kt b/core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionResult.kt
new file mode 100644
index 0000000..14bfdff
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/permissions/PermissionResult.kt
@@ -0,0 +1,26 @@
+package ca.allanwang.kau.permissions
+
+import android.content.pm.PackageManager
+
+/**
+ * Created by Allan Wang on 2017-07-03.
+ */
+class PermissionResult(permissions: Array<out String>, val callback: (granted: Boolean, deniedPerm: String?) -> Unit) {
+ val permissions = mutableSetOf(*permissions)
+
+ /**
+ * Called from the manager whenever a permission has changed
+ * Returns true if result is completed, false otherwise
+ */
+ fun onResult(permission: String, result: Int): Boolean {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ callback(false, permission)
+ permissions.clear()
+ return true
+ }
+ permissions.remove(permission)
+ if (permissions.isNotEmpty()) return false
+ callback(true, null)
+ return true
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/permissions/Permissions.kt b/core/src/main/kotlin/ca/allanwang/kau/permissions/Permissions.kt
new file mode 100644
index 0000000..fd43102
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/permissions/Permissions.kt
@@ -0,0 +1,68 @@
+package ca.allanwang.kau.permissions
+
+import android.Manifest
+import android.app.Activity
+import android.content.Context
+
+
+/**
+ * Created by Allan Wang on 2017-07-02.
+ *
+ * Bindings for the permission manager
+ */
+
+/**
+ * Hook that should be added inside all [Activity.onRequestPermissionsResult] so that the Permission manager can handle the responses
+ */
+fun Activity.kauOnRequestPermissionsResult(permissions: Array<out String>, grantResults: IntArray)
+ = PermissionManager.onRequestPermissionsResult(this, permissions, grantResults)
+
+/**
+ * Request a permission with a callback
+ * In reality, an activity is needed to fulfill the request, but a context is enough if those permissions are already granted
+ * To be safe, you may want to check that the context can be casted successfully first
+ * The [callback] returns [granted], which is true if all permissions are granted
+ * [deniedPerm] is the first denied permission, if granted is false
+ */
+fun Context.kauRequestPermissions(vararg permissions: String, callback: (granted: Boolean, deniedPerm: String?) -> Unit)
+ = PermissionManager(this, permissions, callback)
+
+/**
+ * See http://developer.android.com/guide/topics/security/permissions.html#normal-dangerous for a
+ * list of 'dangerous' permissions that require a permission request on API 23.
+ */
+const val PERMISSION_READ_CALENDAR = Manifest.permission.READ_CALENDAR
+
+const val PERMISSION_WRITE_CALENDAR = Manifest.permission.WRITE_CALENDAR
+
+const val PERMISSION_CAMERA = Manifest.permission.CAMERA
+
+const val PERMISSION_READ_CONTACTS = Manifest.permission.READ_CONTACTS
+const val PERMISSION_WRITE_CONTACTS = Manifest.permission.WRITE_CONTACTS
+const val PERMISSION_GET_ACCOUNTS = Manifest.permission.GET_ACCOUNTS
+
+const val PERMISSION_ACCESS_FINE_LOCATION = Manifest.permission.ACCESS_FINE_LOCATION
+const val PERMISSION_ACCESS_COARSE_LOCATION = Manifest.permission.ACCESS_COARSE_LOCATION
+
+const val PERMISSION_RECORD_AUDIO = Manifest.permission.RECORD_AUDIO
+
+const val PERMISSION_READ_PHONE_STATE = Manifest.permission.READ_PHONE_STATE
+const val PERMISSION_CALL_PHONE = Manifest.permission.CALL_PHONE
+const val PERMISSION_READ_CALL_LOG = Manifest.permission.READ_CALL_LOG
+const val PERMISSION_WRITE_CALL_LOG = Manifest.permission.WRITE_CALL_LOG
+const val PERMISSION_ADD_VOICEMAIL = Manifest.permission.ADD_VOICEMAIL
+const val PERMISSION_USE_SIP = Manifest.permission.USE_SIP
+const val PERMISSION_PROCESS_OUTGOING_CALLS = Manifest.permission.PROCESS_OUTGOING_CALLS
+
+const val PERMISSION_BODY_SENSORS = Manifest.permission.BODY_SENSORS
+
+const val PERMISSION_SEND_SMS = Manifest.permission.SEND_SMS
+const val PERMISSION_RECEIVE_SMS = Manifest.permission.RECEIVE_SMS
+const val PERMISSION_READ_SMS = Manifest.permission.READ_SMS
+const val PERMISSION_RECEIVE_WAP_PUSH = Manifest.permission.RECEIVE_WAP_PUSH
+const val PERMISSION_RECEIVE_MMS = Manifest.permission.RECEIVE_MMS
+
+const val PERMISSION_READ_EXTERNAL_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE
+const val PERMISSION_WRITE_EXTERNAL_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE
+
+const val PERMISSION_SYSTEM_ALERT_WINDOW = Manifest.permission.SYSTEM_ALERT_WINDOW \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt b/core/src/main/kotlin/ca/allanwang/kau/searchview/SearchItem.kt
new file mode 100644
index 0000000..ac8ec2e
--- /dev/null
+++ b/core/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.id.kau_item_search,
+ R.layout.kau_search_iitem,
+ {ViewHolder(it)}
+) {
+
+ 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.search_icon)
+ val title: TextView by bindView(R.id.search_title)
+ val desc: TextView by bindView(R.id.search_desc)
+ val container: ConstraintLayout by bindView(R.id.search_item_frame)
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt b/core/src/main/kotlin/ca/allanwang/kau/searchview/SearchView.kt
new file mode 100644
index 0000000..c077a06
--- /dev/null
+++ b/core/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.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.search_shadow)
+ private val card: BoundedCardView 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)
+ val iconExtra: ImageView by bindView(R.id.search_extra)
+ private val iconClear: ImageView by bindView(R.id.search_clear)
+ private val divider: View by bindView(R.id.search_divider)
+ private val recycler: RecyclerView by bindView(R.id.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/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt
new file mode 100644
index 0000000..ec51bfd
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ActivityUtils.kt
@@ -0,0 +1,71 @@
+package ca.allanwang.kau.utils
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Color
+import android.support.annotation.ColorInt
+import android.support.annotation.StringRes
+import android.support.design.widget.Snackbar
+import android.support.v4.app.Fragment
+import android.view.Menu
+import ca.allanwang.kau.R
+import com.mikepenz.iconics.typeface.IIcon
+import org.jetbrains.anko.contentView
+import org.jetbrains.anko.withArguments
+
+/**
+ * Created by Allan Wang on 2017-06-21.
+ */
+
+/**
+ * Restarts an activity from itself without animations
+ * Keeps its existing extra bundles and has a builder to accept other parameters
+ */
+fun Activity.restart(builder: Intent.() -> Unit = {}) {
+ val i = Intent(this, this::class.java)
+ i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ i.putExtras(intent.extras)
+ i.builder()
+ startActivity(i)
+ overridePendingTransition(0, 0) //No transitions
+ finish()
+ overridePendingTransition(0, 0)
+}
+
+fun Activity.finishSlideOut() {
+ finish()
+ overridePendingTransition(R.anim.kau_fade_in, R.anim.kau_slide_out_right_top)
+}
+
+var Activity.navigationBarColor: Int
+ get() = if (buildIsLollipopAndUp) window.navigationBarColor else Color.BLACK
+ set(value) {
+ if (buildIsLollipopAndUp) window.navigationBarColor = value
+ }
+
+var Activity.statusBarColor: Int
+ get() = if (buildIsLollipopAndUp) window.statusBarColor else Color.BLACK
+ set(value) {
+ if (buildIsLollipopAndUp) window.statusBarColor = value
+ }
+
+/**
+ * Themes the base menu icons and adds iicons programmatically based on ids
+ *
+ * Call in [Activity.onCreateOptionsMenu]
+ */
+fun Activity.setMenuIcons(menu: Menu, @ColorInt color: Int = Color.WHITE, vararg iicons: Pair<Int, IIcon>) {
+ iicons.forEach { (id, iicon) ->
+ menu.findItem(id).icon = iicon.toDrawable(this, sizeDp = 18, color = color)
+ }
+}
+
+fun Activity.hideKeyboard() = currentFocus.hideKeyboard()
+
+fun Activity.showKeyboard() = currentFocus.showKeyboard()
+
+fun Activity.snackbar(text: String, duration: Int = Snackbar.LENGTH_LONG, builder: Snackbar.() -> Unit = {})
+ = contentView!!.snackbar(text, duration, builder)
+
+fun Activity.snackbar(@StringRes textId: Int, duration: Int = Snackbar.LENGTH_LONG, builder: Snackbar.() -> Unit = {})
+ = contentView!!.snackbar(textId, duration, builder) \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt
new file mode 100644
index 0000000..3db8b9c
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt
@@ -0,0 +1,15 @@
+package ca.allanwang.kau.utils
+
+import ca.allanwang.kau.kotlin.lazyInterpolator
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ *
+ * Holder for a bunch of common animators/interpolators used throughout this library
+ */
+object AnimHolder {
+
+ val fastOutSlowInInterpolator = lazyInterpolator(android.R.interpolator.fast_out_linear_in)
+ val decelerateInterpolator = lazyInterpolator(android.R.interpolator.decelerate_cubic)
+
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
new file mode 100644
index 0000000..86b049e
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
@@ -0,0 +1,153 @@
+package ca.allanwang.kau.utils
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.content.Context
+import android.support.annotation.StringRes
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.widget.TextView
+import ca.allanwang.kau.kotlin.lazyContext
+
+/**
+ * Created by Allan Wang on 2017-06-01.
+ *
+ * Animation extension @KauUtils functions for Views
+ */
+@KauUtils fun View.rootCircularReveal(x: Int = 0, y: Int = 0, duration: Long = 500L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ this.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
+ override @KauUtils fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int,
+ oldRight: Int, oldBottom: Int) {
+ v.removeOnLayoutChangeListener(this)
+ var x2 = x
+ var y2 = y
+ if (x2 > right) x2 = 0
+ if (y2 > bottom) y2 = 0
+ val radius = Math.hypot(Math.max(x2, right - x2).toDouble(), Math.max(y2, bottom - y2).toDouble()).toInt()
+ val reveal = ViewAnimationUtils.createCircularReveal(v, x2, y2, 0f, radius.toFloat())
+ reveal.interpolator = DecelerateInterpolator(1f)
+ reveal.duration = duration
+ reveal.addListener(object : AnimatorListenerAdapter() {
+ override @KauUtils fun onAnimationStart(animation: Animator?) {
+ visible()
+ onStart?.invoke()
+ }
+
+ override @KauUtils fun onAnimationEnd(animation: Animator?) = onFinish?.invoke() ?: Unit
+ override @KauUtils fun onAnimationCancel(animation: Animator?) = onFinish?.invoke() ?: Unit
+ })
+ reveal.start()
+ }
+ })
+}
+
+@KauUtils fun View.circularReveal(x: Int = 0, y: Int = 0, offset: Long = 0L, radius: Float = -1.0f, duration: Long = 500L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ visible()
+ onFinish?.invoke()
+ return
+ }
+ var r = radius
+ if (r < 0.0f) {
+ r = Math.max(Math.hypot(x.toDouble(), y.toDouble()), Math.hypot((width - x.toDouble()), (height - y.toDouble()))).toFloat()
+ }
+ val anim = ViewAnimationUtils.createCircularReveal(this, x, y, 0f, r).setDuration(duration)
+ anim.startDelay = offset
+ anim.addListener(object : AnimatorListenerAdapter() {
+ override @KauUtils fun onAnimationStart(animation: Animator?) {
+ visible()
+ onStart?.invoke()
+ }
+
+ override @KauUtils fun onAnimationEnd(animation: Animator?) = onFinish?.invoke() ?: Unit
+ override @KauUtils fun onAnimationCancel(animation: Animator?) = onFinish?.invoke() ?: Unit
+ })
+ anim.start()
+}
+
+@KauUtils fun View.circularHide(x: Int = 0, y: Int = 0, offset: Long = 0L, radius: Float = -1.0f, duration: Long = 500L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ invisible()
+ onFinish?.invoke()
+ return
+ }
+ var r = radius
+ if (r < 0.0f) {
+ r = Math.max(Math.hypot(x.toDouble(), y.toDouble()), Math.hypot((width - x.toDouble()), (height - y.toDouble()))).toFloat()
+ }
+ val anim = ViewAnimationUtils.createCircularReveal(this, x, y, r, 0f).setDuration(duration)
+ anim.startDelay = offset
+ anim.addListener(object : AnimatorListenerAdapter() {
+ override @KauUtils fun onAnimationStart(animation: Animator?) = onStart?.invoke() ?: Unit
+
+ override @KauUtils fun onAnimationEnd(animation: Animator?) {
+ invisible()
+ onFinish?.invoke() ?: Unit
+ }
+
+ override @KauUtils fun onAnimationCancel(animation: Animator?) = onFinish?.invoke() ?: Unit
+ })
+ anim.start()
+}
+
+@KauUtils fun View.fadeIn(offset: Long = 0L, duration: Long = 200L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ visible()
+ onFinish?.invoke()
+ return
+ }
+ if (isAttachedToWindow) {
+ val anim = AnimationUtils.loadAnimation(context, android.R.anim.fade_in)
+ anim.startOffset = offset
+ anim.duration = duration
+ anim.setAnimationListener(object : Animation.AnimationListener {
+ override @KauUtils fun onAnimationRepeat(animation: Animation?) {}
+ override @KauUtils fun onAnimationEnd(animation: Animation?) = onFinish?.invoke() ?: Unit
+ override @KauUtils fun onAnimationStart(animation: Animation?) {
+ visible()
+ onStart?.invoke()
+ }
+ })
+ startAnimation(anim)
+ }
+}
+
+@KauUtils fun View.fadeOut(offset: Long = 0L, duration: Long = 200L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ invisible()
+ onFinish?.invoke()
+ return
+ }
+ val anim = AnimationUtils.loadAnimation(context, android.R.anim.fade_out)
+ anim.startOffset = offset
+ anim.duration = duration
+ anim.setAnimationListener(object : Animation.AnimationListener {
+ override @KauUtils fun onAnimationRepeat(animation: Animation?) {}
+ override @KauUtils fun onAnimationEnd(animation: Animation?) {
+ invisible()
+ onFinish?.invoke()
+ }
+
+ override @KauUtils fun onAnimationStart(animation: Animation?) {
+ onStart?.invoke()
+ }
+ })
+ startAnimation(anim)
+}
+
+@KauUtils fun TextView.setTextWithFade(text: String, duration: Long = 200, onFinish: (() -> Unit)? = null) {
+ fadeOut(duration = duration, onFinish = {
+ setText(text)
+ fadeIn(duration = duration, onFinish = onFinish)
+ })
+}
+
+@KauUtils fun TextView.setTextWithFade(@StringRes textId: Int, duration: Long = 200, onFinish: (() -> Unit)? = null) = setTextWithFade(context.getString(textId), duration, onFinish) \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
new file mode 100644
index 0000000..8590d6f
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
@@ -0,0 +1,235 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.support.annotation.ColorInt
+import android.support.annotation.FloatRange
+import android.support.annotation.IntRange
+import android.support.v4.content.ContextCompat
+import android.support.v4.graphics.drawable.DrawableCompat
+import android.support.v7.widget.AppCompatEditText
+import android.support.v7.widget.Toolbar
+import android.widget.*
+import com.afollestad.materialdialogs.R
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+fun Int.isColorDark(): Boolean
+ = (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255.0 < 0.5
+
+fun Int.toHexString(withAlpha: Boolean = false, withHexPrefix: Boolean = true): String {
+ val hex = if (withAlpha) String.format("#%08X", this)
+ else String.format("#%06X", 0xFFFFFF and this)
+ return if (withHexPrefix) hex else hex.substring(1)
+}
+
+fun Int.toRgbaString(): String = "rgba(${Color.red(this)}, ${Color.green(this)}, ${Color.blue(this)}, ${(Color.alpha(this) / 255f).round(3)})"
+
+fun Int.toHSV(): FloatArray {
+ val hsv = FloatArray(3)
+ Color.colorToHSV(this, hsv)
+ return hsv
+}
+
+fun FloatArray.toColor(): Int = Color.HSVToColor(this)
+
+fun Int.isColorVisibleOn(@ColorInt color: Int, @IntRange(from = 0L, to = 255L) delta: Int = 25,
+ @IntRange(from = 0L, to = 255L) minAlpha: Int = 50): Boolean =
+ if (Color.alpha(this) < minAlpha) false
+ else !(Math.abs(Color.red(this) - Color.red(color)) < delta
+ && Math.abs(Color.green(this) - Color.green(color)) < delta
+ && Math.abs(Color.blue(this) - Color.blue(color)) < delta)
+
+
+@ColorInt
+fun Context.getDisabledColor(): Int {
+ val primaryColor = resolveColor(android.R.attr.textColorPrimary)
+ val disabledColor = if (primaryColor.isColorDark()) Color.BLACK else Color.WHITE
+ return disabledColor.adjustAlpha(0.3f)
+}
+
+@ColorInt
+fun Int.adjustAlpha(factor: Float): Int {
+ val alpha = Math.round(Color.alpha(this) * factor)
+ return Color.argb(alpha, Color.red(this), Color.green(this), Color.blue(this))
+}
+
+val Int.isColorTransparent: Boolean
+ get() = Color.alpha(this) != 255
+
+@ColorInt
+fun Int.withAlpha(@IntRange(from = 0L, to = 255L) alpha: Int): Int
+ = Color.argb(alpha, Color.red(this), Color.green(this), Color.blue(this))
+
+@ColorInt
+fun Int.withMinAlpha(@IntRange(from = 0L, to = 255L) alpha: Int): Int
+ = Color.argb(Math.max(alpha, Color.alpha(this)), Color.red(this), Color.green(this), Color.blue(this))
+
+@ColorInt
+fun Int.lighten(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int {
+ val (red, green, blue) = intArrayOf(Color.red(this), Color.green(this), Color.blue(this))
+ .map { (it * (1f - factor) + 255f * factor).toInt() }
+ return Color.argb(Color.alpha(this), red, green, blue)
+}
+
+@ColorInt
+fun Int.darken(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int {
+ val (red, green, blue) = intArrayOf(Color.red(this), Color.green(this), Color.blue(this))
+ .map { (it * (1f - factor)).toInt() }
+ return Color.argb(Color.alpha(this), red, green, blue)
+}
+
+@ColorInt
+fun Int.colorToBackground(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int
+ = if (isColorDark()) darken(factor) else lighten(factor)
+
+@ColorInt
+fun Int.colorToForeground(@FloatRange(from = 0.0, to = 1.0) factor: Float = 0.1f): Int
+ = if (isColorDark()) lighten(factor) else darken(factor)
+
+@Throws(IllegalArgumentException::class)
+fun String.toColor(): Int {
+ val toParse: String
+ if (startsWith("#") && length == 4)
+ toParse = "#${this[1]}${this[1]}${this[2]}${this[2]}${this[3]}${this[3]}"
+ else
+ toParse = this
+ return Color.parseColor(toParse)
+}
+
+//Get ColorStateList
+fun Context.colorStateList(@ColorInt color: Int): ColorStateList {
+ val disabledColor = color.adjustAlpha(0.3f)
+ return ColorStateList(arrayOf(intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked),
+ intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked),
+ intArrayOf(-android.R.attr.state_enabled, -android.R.attr.state_checked),
+ intArrayOf(-android.R.attr.state_enabled, android.R.attr.state_checked)),
+ intArrayOf(color.adjustAlpha(0.8f), color, disabledColor, disabledColor))
+}
+
+/*
+ * Tint Helpers
+ * Kotlin tint bindings that start with 'tint' so it doesn't conflict with existing methods
+ * Largely based on MDTintHelper
+ * https://github.com/afollestad/material-dialogs/blob/master/core/src/main/java/com/afollestad/materialdialogs/internal/MDTintHelper.java
+ */
+fun RadioButton.tint(colors: ColorStateList) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ buttonTintList = colors
+ } else {
+ val radioDrawable = context.drawable(R.drawable.abc_btn_radio_material)
+ val d = DrawableCompat.wrap(radioDrawable)
+ DrawableCompat.setTintList(d, colors)
+ buttonDrawable = d
+ }
+}
+
+fun RadioButton.tint(@ColorInt color: Int) = tint(context.colorStateList(color))
+
+fun CheckBox.tint(colors: ColorStateList) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ buttonTintList = colors
+ } else {
+ val checkDrawable = context.drawable(R.drawable.abc_btn_check_material)
+ val drawable = DrawableCompat.wrap(checkDrawable)
+ DrawableCompat.setTintList(drawable, colors)
+ buttonDrawable = drawable
+ }
+}
+
+fun CheckBox.tint(@ColorInt color: Int) = tint(context.colorStateList(color))
+
+fun SeekBar.tint(@ColorInt color: Int) {
+ val s1 = ColorStateList.valueOf(color)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ thumbTintList = s1
+ progressTintList = s1
+ } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
+ val progressDrawable = DrawableCompat.wrap(progressDrawable)
+ this.progressDrawable = progressDrawable
+ DrawableCompat.setTintList(progressDrawable, s1)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ val thumbDrawable = DrawableCompat.wrap(thumb)
+ DrawableCompat.setTintList(thumbDrawable, s1)
+ thumb = thumbDrawable
+ }
+ } else {
+ val mode: PorterDuff.Mode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1)
+ PorterDuff.Mode.MULTIPLY else PorterDuff.Mode.SRC_IN
+ indeterminateDrawable?.setColorFilter(color, mode)
+ progressDrawable?.setColorFilter(color, mode)
+ }
+}
+
+fun ProgressBar.tint(@ColorInt color: Int, skipIndeterminate: Boolean = false) {
+ val sl = ColorStateList.valueOf(color)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ progressTintList = sl
+ secondaryProgressTintList = sl
+ if (!skipIndeterminate) indeterminateTintList = sl
+ } else {
+ val mode: PorterDuff.Mode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1)
+ PorterDuff.Mode.MULTIPLY else PorterDuff.Mode.SRC_IN
+ indeterminateDrawable?.setColorFilter(color, mode)
+ progressDrawable?.setColorFilter(color, mode)
+ }
+}
+
+fun Context.textColorStateList(@ColorInt color: Int): ColorStateList {
+ val states = arrayOf(
+ intArrayOf(-android.R.attr.state_enabled),
+ intArrayOf(-android.R.attr.state_pressed, -android.R.attr.state_focused),
+ intArrayOf()
+ )
+ val colors = intArrayOf(
+ resolveColor(R.attr.colorControlNormal),
+ resolveColor(R.attr.colorControlNormal),
+ color
+ )
+ return ColorStateList(states, colors)
+}
+
+fun EditText.tint(@ColorInt color: Int) {
+ val editTextColorStateList = context.textColorStateList(color)
+ if (this is AppCompatEditText) {
+ supportBackgroundTintList = editTextColorStateList
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ backgroundTintList = editTextColorStateList
+ }
+ tintCursor(color)
+}
+
+fun EditText.tintCursor(@ColorInt color: Int) {
+ try {
+ val fCursorDrawableRes = TextView::class.java.getDeclaredField("mCursorDrawableRes")
+ fCursorDrawableRes.isAccessible = true
+ val mCursorDrawableRes = fCursorDrawableRes.getInt(this)
+ val fEditor = TextView::class.java.getDeclaredField("mEditor")
+ fEditor.isAccessible = true
+ val editor = fEditor.get(this)
+ val clazz = editor.javaClass
+ val fCursorDrawable = clazz.getDeclaredField("mCursorDrawable")
+ fCursorDrawable.isAccessible = true
+ val drawables: Array<Drawable> = Array(2, {
+ val drawable = ContextCompat.getDrawable(context, mCursorDrawableRes)
+ drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN)
+ drawable
+ })
+ fCursorDrawable.set(editor, drawables)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+}
+
+fun Toolbar.tint(@ColorInt color: Int, tintTitle: Boolean = true) {
+ if (tintTitle) {
+ setTitleTextColor(color)
+ setSubtitleTextColor(color)
+ }
+ (0 until childCount).asSequence().forEach { (getChildAt(it) as? ImageButton)?.setColorFilter(color) }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt
new file mode 100644
index 0000000..944caa4
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt
@@ -0,0 +1,6 @@
+package ca.allanwang.kau.utils
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
new file mode 100644
index 0000000..21021e2
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
@@ -0,0 +1,165 @@
+package ca.allanwang.kau.utils
+
+import android.app.Activity
+import android.app.ActivityOptions
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.net.ConnectivityManager
+import android.net.Uri
+import android.os.Bundle
+import android.support.annotation.*
+import android.support.v4.app.ActivityOptionsCompat
+import android.support.v4.content.ContextCompat
+import android.util.TypedValue
+import android.view.View
+import android.widget.Toast
+import ca.allanwang.kau.R
+import ca.allanwang.kau.logging.KL
+import com.afollestad.materialdialogs.MaterialDialog
+
+
+/**
+ * Created by Allan Wang on 2017-06-03.
+ */
+fun Context.startActivity(
+ clazz: Class<out Activity>,
+ clearStack: Boolean = false,
+ transition: Boolean = false,
+ bundle: Bundle? = null,
+ intentBuilder: Intent.() -> Unit = {}) {
+ val intent = (Intent(this, clazz))
+ if (clearStack) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK)
+ intent.intentBuilder()
+ val fullBundle = if (transition && this is Activity) ActivityOptions.makeSceneTransitionAnimation(this).toBundle() else Bundle()
+ if (transition && this !is Activity) KL.d("Cannot make scene transition when context is not an instance of an Activity")
+ if (bundle != null) fullBundle.putAll(bundle)
+ ContextCompat.startActivity(this, intent, if (fullBundle.isEmpty) null else fullBundle)
+ if (this is Activity && clearStack) finish()
+}
+
+/**
+ * Bring in activity from the right
+ */
+fun Context.startActivitySlideIn(clazz: Class<out Activity>, clearStack: Boolean = false, intentBuilder: Intent.() -> Unit = {}, bundleBuilder: Bundle.() -> Unit = {}) {
+ val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_slide_in_right, R.anim.kau_fade_out).toBundle()
+ bundle.bundleBuilder()
+ startActivity(clazz, clearStack, intentBuilder = intentBuilder, bundle = bundle)
+}
+
+/**
+ * Bring in activity from behind while pushing the current activity to the right
+ * This replicates the exit animation of a sliding activity, but is a forward creation
+ * For the animation to work, the previous activity should not be in the stack (otherwise you wouldn't need this in the first place)
+ * Consequently, the stack will be cleared by default
+ */
+fun Context.startActivitySlideOut(clazz: Class<out Activity>, clearStack: Boolean = true, intentBuilder: Intent.() -> Unit = {}, bundleBuilder: Bundle.() -> Unit = {}) {
+ val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.kau_fade_in, R.anim.kau_slide_out_right_top).toBundle()
+ bundle.bundleBuilder()
+ startActivity(clazz, clearStack, intentBuilder = intentBuilder, bundle = bundle)
+}
+
+fun Context.startPlayStoreLink(@StringRes packageIdRes: Int) = startPlayStoreLink(string(packageIdRes))
+
+fun Context.startPlayStoreLink(packageId: String) {
+ startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=$packageId")))
+}
+
+/**
+ * Starts a url
+ * If given a series of links, will open the first one that isn't null
+ */
+fun Context.startLink(vararg url: String?) {
+ val link = url.firstOrNull { !it.isNullOrBlank() } ?: return
+ val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
+ startActivity(browserIntent)
+}
+
+//Toast helpers
+fun Context.toast(@StringRes id: Int, duration: Int = Toast.LENGTH_LONG) = toast(this.string(id), duration)
+
+fun Context.toast(text: String, duration: Int = Toast.LENGTH_LONG) {
+ Toast.makeText(this, text, duration).show()
+}
+
+//Resource retrievers
+fun Context.string(@StringRes id: Int): String = getString(id)
+
+fun Context.string(@StringRes id: Int, fallback: String?): String? = if (id > 0) string(id) else fallback
+fun Context.string(holder: StringHolder?): String? = holder?.getString(this)
+fun Context.color(@ColorRes id: Int): Int = ContextCompat.getColor(this, id)
+fun Context.integer(@IntegerRes id: Int): Int = resources.getInteger(id)
+fun Context.dimen(@DimenRes id: Int): Float = resources.getDimension(id)
+fun Context.dimenPixelSize(@DimenRes id: Int): Int = resources.getDimensionPixelSize(id)
+fun Context.drawable(@DrawableRes id: Int): Drawable = ContextCompat.getDrawable(this, id)
+fun Context.drawable(@DrawableRes id: Int, fallback: Drawable?): Drawable? = if (id > 0) drawable(id) else fallback
+
+//Attr retrievers
+fun Context.resolveColor(@AttrRes attr: Int, fallback: Int = 0): Int {
+ val a = theme.obtainStyledAttributes(intArrayOf(attr))
+ try {
+ return a.getColor(0, fallback)
+ } finally {
+ a.recycle()
+ }
+}
+
+fun Context.resolveDrawable(@AttrRes attr: Int): Drawable? {
+ val a = theme.obtainStyledAttributes(intArrayOf(attr))
+ try {
+ return a.getDrawable(0)
+ } finally {
+ a.recycle()
+ }
+}
+
+fun Context.resolveBoolean(@AttrRes attr: Int, fallback: Boolean = false): Boolean {
+ val a = theme.obtainStyledAttributes(intArrayOf(attr))
+ try {
+ return a.getBoolean(0, fallback)
+ } finally {
+ a.recycle()
+ }
+}
+
+fun Context.resolveString(@AttrRes attr: Int, fallback: String = ""): String {
+ val v = TypedValue()
+ return if (theme.resolveAttribute(attr, v, true)) v.string.toString() else fallback
+}
+
+/**
+ * Wrapper function for the MaterialDialog adapterBuilder
+ * There is no need to call build() or show() as those are done by default
+ */
+inline fun Context.materialDialog(action: MaterialDialog.Builder.() -> Unit): MaterialDialog {
+ val builder = MaterialDialog.Builder(this)
+ builder.action()
+ return builder.show()
+}
+
+inline val Context.isNetworkAvailable: Boolean
+ get() {
+ val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val activeNetworkInfo = connectivityManager.activeNetworkInfo
+ return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
+ }
+
+fun Context.getDip(value: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics)
+
+inline val Context.isRtl: Boolean
+ get() = resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL
+
+/**
+ * Determine if the navigation bar will be on the bottom of the screen, based on logic in
+ * PhoneWindowManager.
+ */
+inline val Context.isNavBarOnBottom: Boolean
+ get() {
+ val cfg = resources.configuration
+ val dm = resources.displayMetrics
+ val canMove = dm.widthPixels != dm.heightPixels && cfg.smallestScreenWidthDp < 600
+ return !canMove || dm.widthPixels < dm.heightPixels
+ }
+
+fun Context.hasPermission(permissions: String) = !buildIsMarshmallowAndUp || ContextCompat.checkSelfPermission(this, permissions) == PackageManager.PERMISSION_GRANTED \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Either.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Either.kt
new file mode 100644
index 0000000..dab5810
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Either.kt
@@ -0,0 +1,32 @@
+package ca.allanwang.kau.utils
+
+/**
+ * Created by Allan Wang on 2017-06-17.
+ *
+ * Courtesy of adelnizamutdinov
+ *
+ * https://github.com/adelnizamutdinov/kotlin-either
+ */
+@Suppress("unused")
+sealed class Either<out L, out R>
+
+data class Left<out T>(val value: T) : Either<T, Nothing>()
+data class Right<out T>(val value: T) : Either<Nothing, T>()
+
+inline fun <L, R, T> Either<L, R>.fold(left: (L) -> T, right: (R) -> T): T =
+ when (this) {
+ is Left -> left(value)
+ is Right -> right(value)
+ }
+
+inline fun <L, R, T> Either<L, R>.flatMap(f: (R) -> Either<L, T>): Either<L, T> =
+ fold({ this as Left }, f)
+
+inline fun <L, R, T> Either<L, R>.map(f: (R) -> T): Either<L, T> =
+ flatMap { Right(f(it)) }
+
+val <T> Either<T, *>.isLeft: Boolean
+ get() = this is Left<T>
+
+val <T> Either<*, T>.isRight: Boolean
+ get() = this is Right<T> \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt
new file mode 100644
index 0000000..3fc509d
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt
@@ -0,0 +1,29 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.graphics.Typeface
+
+/**
+ * Created by Allan Wang on 2017-06-28.
+ */
+object FontUtils {
+
+ val sTypefaceCache: MutableMap<String, Typeface> = mutableMapOf()
+
+ fun get(context: Context, font: String): Typeface {
+ synchronized(sTypefaceCache) {
+ if (!sTypefaceCache.containsKey(font)) {
+ val tf = Typeface.createFromAsset(
+ context.applicationContext.assets, "fonts/$font.ttf")
+ sTypefaceCache.put(font, tf)
+ }
+ return sTypefaceCache.get(font) ?: throw IllegalArgumentException("Font error; typeface does not exist at assets/fonts$font.ttf")
+ }
+ }
+
+ fun getName(typeface: Typeface): String? = sTypefaceCache.entries.firstOrNull { it.value == typeface }?.key
+
+}
+
+fun Context.getFont(font: String) = FontUtils.get(this, font)
+fun Context.getFontName(typeface: Typeface) = FontUtils.getName(typeface) \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt
new file mode 100644
index 0000000..acc71f2
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt
@@ -0,0 +1,12 @@
+package ca.allanwang.kau.utils
+
+import android.support.v4.app.Fragment
+import org.jetbrains.anko.bundleOf
+
+/**
+ * Created by Allan Wang on 2017-07-02.
+ */
+fun <T : Fragment> T.withArguments(vararg params: Pair<String, Any>): T {
+ arguments = bundleOf(*params)
+ return this
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt
new file mode 100644
index 0000000..03a1605
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt
@@ -0,0 +1,20 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.support.annotation.ColorInt
+import com.mikepenz.iconics.IconicsDrawable
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ */
+@KauUtils fun IIcon.toDrawable(c: Context, sizeDp: Int = 24, @ColorInt color: Int = Color.WHITE, builder: IconicsDrawable.() -> Unit = {}): Drawable {
+ val state = ColorStateList.valueOf(color)
+ val icon = IconicsDrawable(c).icon(this).sizeDp(sizeDp)
+ icon.setTintList(state)
+ icon.builder()
+ return icon
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt
new file mode 100644
index 0000000..247bbc7
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt
@@ -0,0 +1,166 @@
+package ca.allanwang.kau.utils
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ *
+ * Courtesy of Jake Wharton
+ *
+ * https://github.com/JakeWharton/kotterknife/blob/master/src/main/kotlin/kotterknife/ButterKnife.kt
+ */
+import android.app.Activity
+import android.app.Dialog
+import android.app.DialogFragment
+import android.app.Fragment
+import android.support.v7.widget.RecyclerView.ViewHolder
+import android.view.View
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+import android.support.v4.app.DialogFragment as SupportDialogFragment
+import android.support.v4.app.Fragment as SupportFragment
+
+fun <V : View> View.bindView(id: Int)
+ : ReadOnlyProperty<View, V> = required(id, viewFinder)
+
+fun <V : View> Activity.bindView(id: Int)
+ : ReadOnlyProperty<Activity, V> = required(id, viewFinder)
+
+fun <V : View> Dialog.bindView(id: Int)
+ : ReadOnlyProperty<Dialog, V> = required(id, viewFinder)
+
+fun <V : View> DialogFragment.bindView(id: Int)
+ : ReadOnlyProperty<DialogFragment, V> = required(id, viewFinder)
+
+fun <V : View> android.support.v4.app.DialogFragment.bindView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, V> = required(id, viewFinder)
+
+fun <V : View> Fragment.bindView(id: Int)
+ : ReadOnlyProperty<Fragment, V> = required(id, viewFinder)
+
+fun <V : View> android.support.v4.app.Fragment.bindView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, V> = required(id, viewFinder)
+
+fun <V : View> ViewHolder.bindView(id: Int)
+ : ReadOnlyProperty<ViewHolder, V> = required(id, viewFinder)
+
+fun <V : View> View.bindOptionalView(id: Int)
+ : ReadOnlyProperty<View, V?> = optional(id, viewFinder)
+
+fun <V : View> Activity.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Activity, V?> = optional(id, viewFinder)
+
+fun <V : View> Dialog.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Dialog, V?> = optional(id, viewFinder)
+
+fun <V : View> DialogFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<DialogFragment, V?> = optional(id, viewFinder)
+
+fun <V : View> android.support.v4.app.DialogFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, V?> = optional(id, viewFinder)
+
+fun <V : View> Fragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Fragment, V?> = optional(id, viewFinder)
+
+fun <V : View> android.support.v4.app.Fragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, V?> = optional(id, viewFinder)
+
+fun <V : View> ViewHolder.bindOptionalView(id: Int)
+ : ReadOnlyProperty<ViewHolder, V?> = optional(id, viewFinder)
+
+fun <V : View> View.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<View, List<V>> = required(ids, viewFinder)
+
+fun <V : View> Activity.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Activity, List<V>> = required(ids, viewFinder)
+
+fun <V : View> Dialog.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Dialog, List<V>> = required(ids, viewFinder)
+
+fun <V : View> DialogFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<DialogFragment, List<V>> = required(ids, viewFinder)
+
+fun <V : View> android.support.v4.app.DialogFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, List<V>> = required(ids, viewFinder)
+
+fun <V : View> Fragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Fragment, List<V>> = required(ids, viewFinder)
+
+fun <V : View> android.support.v4.app.Fragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, List<V>> = required(ids, viewFinder)
+
+fun <V : View> ViewHolder.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<ViewHolder, List<V>> = required(ids, viewFinder)
+
+fun <V : View> View.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<View, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> Activity.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Activity, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> Dialog.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Dialog, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> DialogFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<DialogFragment, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> android.support.v4.app.DialogFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> Fragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Fragment, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> android.support.v4.app.Fragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, List<V>> = optional(ids, viewFinder)
+
+fun <V : View> ViewHolder.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<ViewHolder, List<V>> = optional(ids, viewFinder)
+
+private val View.viewFinder: View.(Int) -> View?
+ get() = { findViewById(it) }
+private val Activity.viewFinder: Activity.(Int) -> View?
+ get() = { findViewById(it) }
+private val Dialog.viewFinder: Dialog.(Int) -> View?
+ get() = { findViewById(it) }
+private val DialogFragment.viewFinder: DialogFragment.(Int) -> View?
+ get() = { dialog.findViewById(it) }
+private val android.support.v4.app.DialogFragment.viewFinder: android.support.v4.app.DialogFragment.(Int) -> View?
+ get() = { dialog.findViewById(it) }
+private val Fragment.viewFinder: Fragment.(Int) -> View?
+ get() = { view.findViewById(it) }
+private val android.support.v4.app.Fragment.viewFinder: android.support.v4.app.Fragment.(Int) -> View?
+ get() = { view!!.findViewById(it) }
+private val ViewHolder.viewFinder: ViewHolder.(Int) -> View?
+ get() = { itemView.findViewById(it) }
+
+private fun viewNotFound(id: Int, desc: KProperty<*>): Nothing =
+ throw IllegalStateException("View ID $id for '${desc.name}' not found.")
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> (t.finder(id) as V?)?.apply { } ?: viewNotFound(id, desc) }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> optional(id: Int, finder: T.(Int) -> View?)
+ = Lazy { t: T, _ -> t.finder(id) as V? }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> required(ids: IntArray, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> optional(ids: IntArray, finder: T.(Int) -> View?)
+ = Lazy { t: T, _ -> ids.map { t.finder(it) as V? }.filterNotNull() }
+
+// Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it
+private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
+ private object EMPTY
+
+ private var value: Any? = EMPTY
+
+ override fun getValue(thisRef: T, property: KProperty<*>): V {
+ if (value == EMPTY) {
+ value = initializer(thisRef, property)
+ }
+ @Suppress("UNCHECKED_CAST")
+ return value as V
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt
new file mode 100644
index 0000000..837c209
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt
@@ -0,0 +1,48 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import android.support.annotation.RequiresApi
+
+/**
+ * Created by Allan Wang on 2017-06-23.
+ */
+
+/**
+ * Checks if a given package is installed
+ * @param packageName packageId
+ * @return true if installed with activity, false otherwise
+ */
+@KauUtils fun Context.isAppInstalled(packageName: String): Boolean {
+ val pm = packageManager
+ var installed: Boolean
+ try {
+ pm.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES)
+ installed = true
+ } catch (e: PackageManager.NameNotFoundException) {
+ installed = false
+ }
+ return installed
+}
+
+val buildIsLollipopAndUp: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+
+val buildIsMarshmallowAndUp: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
+
+val buildIsNougatAndUp: Boolean
+ get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+
+const val INSTALLER_GOOGLE_PLAY_VENDING = "com.android.vending"
+const val INSTALLER_GOOGLE_PLAY_FEEDBACK = "com.google.android.feedback"
+
+val Context.installerPackageName: String?
+ get() = packageManager.getInstallerPackageName(packageName)
+
+val Context.isFromGooglePlay: Boolean
+ get() {
+ val installer = installerPackageName
+ return arrayOf(INSTALLER_GOOGLE_PLAY_FEEDBACK, INSTALLER_GOOGLE_PLAY_VENDING).any { it == installer }
+ } \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt
new file mode 100644
index 0000000..e70a2d1
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt
@@ -0,0 +1,22 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.support.annotation.StringRes
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+class StringHolder {
+ var text: String? = null
+ var textRes: Int = 0
+
+ constructor(@StringRes textRes: Int) {
+ this.textRes = textRes
+ }
+
+ constructor(text: String) {
+ this.text = text
+ }
+
+ fun getString(context: Context) = context.string(textRes, text)
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/TransitionUtils.kt
new file mode 100644
index 0000000..9e668d0
--- /dev/null
+++ b/core/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) {}
+}
+
+@KauUtils fun TransitionSet.addEndListener(onEnd: (transition: Transition) -> Unit) {
+ addListener(TransitionEndListener(onEnd))
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt
new file mode 100644
index 0000000..84794f9
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt
@@ -0,0 +1,111 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.Drawable
+import android.os.Handler
+import android.os.Looper
+import android.support.annotation.IntRange
+import ca.allanwang.kau.R
+import ca.allanwang.kau.logging.KL
+import java.math.RoundingMode
+import java.text.DecimalFormat
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+
+/**
+ * Markers to isolate respective extension @KauUtils functions to their extended class
+ * Avoids having a whole bunch of methods for nested calls
+ */
+@DslMarker
+annotation class KauUtils
+
+@KauUtils val Int.dpToPx: Int
+ get() = (this * Resources.getSystem().displayMetrics.density).toInt()
+
+@KauUtils val Int.pxToDp: Int
+ get() = (this / Resources.getSystem().displayMetrics.density).toInt()
+
+/**
+ * Log whether current state is in the main thread
+ */
+@KauUtils fun checkThread(id: Int) {
+ val status = if (Looper.myLooper() == Looper.getMainLooper()) "is" else "is not"
+ KL.d("$id $status in the main thread")
+}
+
+/**
+ * Converts minute value to string
+ * Whole hours and days will be converted as such, otherwise it will default to x minutes
+ */
+@KauUtils fun Context.minuteToText(minutes: Long): String = with(minutes) {
+ if (this < 0L) string(R.string.kau_none)
+ else if (this == 60L) string(R.string.kau_one_hour)
+ else if (this == 1440L) string(R.string.kau_one_day)
+ else if (this % 1440L == 0L) String.format(string(R.string.kau_x_days), this / 1440L)
+ else if (this % 60L == 0L) String.format(string(R.string.kau_x_hours), this / 60L)
+ else String.format(string(R.string.kau_x_minutes), this)
+}
+
+@KauUtils fun Number.round(@IntRange(from = 1L) decimalCount: Int): String {
+ val expression = StringBuilder().append("#.")
+ (1..decimalCount).forEach { expression.append("#") }
+ val formatter = DecimalFormat(expression.toString())
+ formatter.roundingMode = RoundingMode.HALF_UP
+ return formatter.format(this)
+}
+
+/**
+ * Extracts the bitmap of a drawable, and applies a scale if given
+ * For solid colors, a 1 x 1 pixel will be generated
+ */
+@KauUtils fun Drawable.toBitmap(scaling: Float = 1f, config: Bitmap.Config = Bitmap.Config.ARGB_8888): Bitmap {
+ if (this is BitmapDrawable && bitmap != null) {
+ if (scaling == 1f) return bitmap
+ val width = (bitmap.width * scaling).toInt()
+ val height = (bitmap.height * scaling).toInt()
+ return Bitmap.createScaledBitmap(bitmap, width, height, false)
+ }
+ val bitmap = if (intrinsicWidth <= 0 || intrinsicHeight <= 0)
+ Bitmap.createBitmap(1, 1, config)
+ else
+ Bitmap.createBitmap((intrinsicWidth * scaling).toInt(), (intrinsicHeight * scaling).toInt(), config)
+ val canvas = Canvas(bitmap)
+ setBounds(0, 0, canvas.width, canvas.height)
+ draw(canvas)
+ return bitmap
+}
+
+/**
+ * Use block for autocloseables
+ */
+inline fun <T : AutoCloseable, R> T.use(block: (T) -> R): R {
+ var closed = false
+ try {
+ return block(this)
+ } catch (e: Exception) {
+ closed = true
+ try {
+ close()
+ } catch (closeException: Exception) {
+ e.addSuppressed(closeException)
+ }
+ throw e
+ } finally {
+ if (!closed) {
+ close()
+ }
+ }
+}
+
+fun postDelayed(delay: Long, action: () -> Unit) {
+ Handler().postDelayed(action, delay)
+}
+
+class KauException(message: String) : RuntimeException(message) \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
new file mode 100644
index 0000000..b4752a5
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
@@ -0,0 +1,126 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.Outline
+import android.graphics.Rect
+import android.support.annotation.ColorInt
+import android.support.annotation.StringRes
+import android.support.annotation.TransitionRes
+import android.support.design.widget.Snackbar
+import android.support.transition.AutoTransition
+import android.support.transition.Transition
+import android.support.transition.TransitionInflater
+import android.support.transition.TransitionManager
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewOutlineProvider
+import android.view.inputmethod.InputMethodManager
+import android.widget.ImageView
+import android.widget.TextView
+import ca.allanwang.kau.logging.KL
+import ca.allanwang.kau.views.createSimpleRippleDrawable
+import com.mikepenz.iconics.IconicsDrawable
+import com.mikepenz.iconics.typeface.IIcon
+
+
+/**
+ * Created by Allan Wang on 2017-05-31.
+ */
+@KauUtils fun <T : View> T.visible(): T {
+ visibility = View.VISIBLE
+ return this
+}
+
+@KauUtils fun <T : View> T.invisible(): T {
+ visibility = View.INVISIBLE
+ return this
+}
+
+@KauUtils fun <T : View> T.gone(): T {
+ visibility = View.GONE
+ return this
+}
+
+@KauUtils fun View.isVisible(): Boolean = visibility == View.VISIBLE
+@KauUtils fun View.isInvisible(): Boolean = visibility == View.INVISIBLE
+@KauUtils fun View.isGone(): Boolean = visibility == View.GONE
+
+fun View.snackbar(text: String, duration: Int = Snackbar.LENGTH_LONG, builder: Snackbar.() -> Unit = {}): Snackbar {
+ val snackbar = Snackbar.make(this, text, duration)
+ snackbar.builder()
+ snackbar.show()
+ return snackbar
+}
+
+fun View.snackbar(@StringRes textId: Int, duration: Int = Snackbar.LENGTH_LONG, builder: Snackbar.() -> Unit = {})
+ = snackbar(context.string(textId), duration, builder)
+
+@KauUtils fun TextView.setTextIfValid(@StringRes id: Int) {
+ if (id > 0) text = context.string(id)
+}
+
+@KauUtils fun ImageView.setIcon(icon: IIcon?, sizeDp: Int = 24, @ColorInt color: Int = Color.WHITE, builder: IconicsDrawable.() -> Unit = {}) {
+ if (icon == null) return
+ setImageDrawable(icon.toDrawable(context, sizeDp = sizeDp, color = color, builder = builder))
+}
+
+@KauUtils fun View.hideKeyboard() {
+ clearFocus()
+ (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).hideSoftInputFromWindow(windowToken, 0)
+}
+
+@KauUtils fun View.showKeyboard() {
+ requestFocus()
+ (context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager).showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
+}
+
+@KauUtils fun ViewGroup.transitionAuto(builder: AutoTransition.() -> Unit = {}) {
+ val transition = AutoTransition()
+ transition.builder()
+ TransitionManager.beginDelayedTransition(this, transition)
+}
+
+@KauUtils fun ViewGroup.transitionDelayed(@TransitionRes id: Int, builder: Transition.() -> Unit = {}) {
+ val transition = TransitionInflater.from(context).inflateTransition(id)
+ transition.builder()
+ TransitionManager.beginDelayedTransition(this, transition)
+}
+
+@KauUtils fun View.setRippleBackground(@ColorInt foregroundColor: Int, @ColorInt backgroundColor: Int) {
+ background = createSimpleRippleDrawable(foregroundColor, backgroundColor)
+}
+
+@KauUtils val View.parentViewGroup: ViewGroup
+ get() = parent as ViewGroup
+
+@KauUtils val View.parentVisibleHeight: Int
+ get() {
+ val r = Rect()
+ parentViewGroup.getWindowVisibleDisplayFrame(r)
+ return r.height()
+ }
+
+val CIRCULAR_OUTLINE: ViewOutlineProvider = object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ KL.d("CIRCULAR OUTLINE")
+ outline.setOval(view.paddingLeft,
+ view.paddingTop,
+ view.width - view.paddingRight,
+ view.height - view.paddingBottom)
+ }
+}
+
+/**
+ * Generates a recycler view with match parent and a linearlayoutmanager, since it's so commonly used
+ */
+fun Context.fullLinearRecycler(rvAdapter: RecyclerView.Adapter<*>? = null, configs: RecyclerView.() -> Unit = {}): RecyclerView {
+ return RecyclerView(this).apply {
+ layoutManager = LinearLayoutManager(this@fullLinearRecycler)
+ layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT)
+ if (rvAdapter != null) adapter = rvAdapter
+ configs()
+ }
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/views/BoundedCardView.kt b/core/src/main/kotlin/ca/allanwang/kau/views/BoundedCardView.kt
new file mode 100644
index 0000000..0cb65d0
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/views/BoundedCardView.kt
@@ -0,0 +1,50 @@
+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
+import ca.allanwang.kau.utils.parentVisibleHeight
+
+
+/**
+ * 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 BoundedCardView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : CardView(context, attrs, defStyleAttr) {
+
+ /**
+ * 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.BoundedCardView)
+ maxHeight = styledAttrs.getDimensionPixelSize(R.styleable.BoundedCardView_maxHeight, -1)
+ maxHeightPercent = styledAttrs.getFloat(R.styleable.BoundedCardView_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/core/src/main/kotlin/ca/allanwang/kau/views/CutoutView.kt b/core/src/main/kotlin/ca/allanwang/kau/views/CutoutView.kt
new file mode 100644
index 0000000..023bdb4
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/views/CutoutView.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ca.allanwang.kau.views
+
+import android.content.Context
+import android.graphics.*
+import android.graphics.drawable.Drawable
+import android.text.TextPaint
+import android.util.AttributeSet
+import android.util.DisplayMetrics
+import android.util.TypedValue
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.dimenPixelSize
+import ca.allanwang.kau.utils.getFont
+import ca.allanwang.kau.utils.parentVisibleHeight
+import ca.allanwang.kau.utils.toBitmap
+
+/**
+ * A view which punches out some text from an opaque color block, allowing you to see through it.
+ */
+class CutoutView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+
+ companion object {
+ const val PHI = 1.6182f
+ const val TYPE_TEXT = 100
+ const val TYPE_DRAWABLE = 101
+ }
+
+ private val paint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
+ private var bitmapScaling: Float = 1f
+ private var cutout: Bitmap? = null
+ var foregroundColor = Color.MAGENTA
+ var text: String? = "Text"
+ set(value) {
+ field = value
+ if (value != null) cutoutType = TYPE_TEXT
+ else if (drawable != null) cutoutType = TYPE_DRAWABLE
+ }
+ var cutoutType: Int = TYPE_TEXT
+ private var textSize: Float = 0f
+ private var cutoutY: Float = 0f
+ private var cutoutX: Float = 0f
+ var drawable: Drawable? = null
+ set(value) {
+ field = value
+ if (value != null) cutoutType = TYPE_DRAWABLE
+ else if (text != null) cutoutType = TYPE_TEXT
+ }
+ private var heightPercentage: Float = 0f
+ private var minHeight: Float = 0f
+ private val maxTextSize: Float
+
+ init {
+ if (attrs != null) {
+ val a = context.obtainStyledAttributes(attrs, R.styleable.CutoutView, 0, 0)
+ if (a.hasValue(R.styleable.CutoutView_font))
+ paint.typeface = context.getFont(a.getString(R.styleable.CutoutView_font))
+ foregroundColor = a.getColor(R.styleable.CutoutView_foregroundColor, foregroundColor)
+ text = a.getString(R.styleable.CutoutView_android_text) ?: text
+ minHeight = a.getDimension(R.styleable.CutoutView_android_minHeight, minHeight)
+ heightPercentage = a.getFloat(R.styleable.CutoutView_heightPercentageToScreen, heightPercentage)
+ a.recycle()
+ }
+ maxTextSize = context.dimenPixelSize(R.dimen.kau_display_4_text_size).toFloat()
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ calculatePosition()
+ createBitmap()
+ }
+
+ private fun calculatePosition() {
+ when (cutoutType) {
+ TYPE_TEXT -> calculateTextPosition()
+ TYPE_DRAWABLE -> calculateImagePosition()
+ }
+ }
+
+ private fun calculateTextPosition() {
+ val targetWidth = width / PHI
+ textSize = getSingleLineTextSize(text!!, paint, targetWidth, 0f, maxTextSize,
+ 0.5f, resources.displayMetrics)
+ paint.textSize = textSize
+
+ // measuring text is fun :] see: https://chris.banes.me/2014/03/27/measuring-text/
+ cutoutX = (width - paint.measureText(text)) / 2
+ val textBounds = Rect()
+ paint.getTextBounds(text, 0, text!!.length, textBounds)
+ val textHeight = textBounds.height().toFloat()
+ cutoutY = (height + textHeight) / 2
+ }
+
+ private fun calculateImagePosition() {
+ if (drawable!!.intrinsicHeight <= 0 || drawable!!.intrinsicWidth <= 0) throw IllegalArgumentException("Drawable's intrinsic size cannot be less than 0")
+ val targetWidth = width / PHI
+ val targetHeight = height / PHI
+ bitmapScaling = Math.min(targetHeight / drawable!!.intrinsicHeight, targetWidth / drawable!!.intrinsicWidth)
+ cutoutX = (width - drawable!!.intrinsicWidth * bitmapScaling) / 2
+ cutoutY = (height - drawable!!.intrinsicHeight * bitmapScaling) / 2
+ }
+
+ /**
+ * If height percent is specified, ensure it is met
+ */
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val minHeight = Math.max(minHeight, heightPercentage * parentVisibleHeight)
+ val trueHeightMeasureSpec = if (minHeight > 0)
+ MeasureSpec.makeMeasureSpec(Math.max(minHeight.toInt(), measuredHeight), MeasureSpec.EXACTLY)
+ else heightMeasureSpec
+ super.onMeasure(widthMeasureSpec, trueHeightMeasureSpec)
+ }
+
+ /**
+ * Recursive binary search to find the best size for the text.
+
+ * Adapted from https://github.com/grantland/android-autofittextview
+ */
+ fun getSingleLineTextSize(text: String,
+ paint: TextPaint,
+ targetWidth: Float,
+ low: Float,
+ high: Float,
+ precision: Float,
+ metrics: DisplayMetrics): Float {
+ val mid = (low + high) / 2.0f
+
+ paint.textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, mid, metrics)
+ val maxLineWidth = paint.measureText(text)
+
+ return if (high - low < precision) low
+ else if (maxLineWidth > targetWidth) getSingleLineTextSize(text, paint, targetWidth, low, mid, precision, metrics)
+ else if (maxLineWidth < targetWidth) getSingleLineTextSize(text, paint, targetWidth, mid, high, precision, metrics)
+ else mid
+ }
+
+ private fun createBitmap() {
+ if (!(cutout?.isRecycled ?: true))
+ cutout?.recycle()
+ if (width == 0 || height == 0) return
+ cutout = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
+ cutout!!.setHasAlpha(true)
+ val cutoutCanvas = Canvas(cutout!!)
+ cutoutCanvas.drawColor(foregroundColor)
+ paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
+
+ when (cutoutType) {
+ TYPE_TEXT -> {
+ // this is the magic – Clear mode punches out the bitmap
+ paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
+ cutoutCanvas.drawText(text, cutoutX, cutoutY, paint)
+ }
+ TYPE_DRAWABLE -> {
+ cutoutCanvas.drawBitmap(drawable!!.toBitmap(bitmapScaling, Bitmap.Config.ALPHA_8), cutoutX, cutoutY, paint)
+ }
+
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.drawBitmap(cutout!!, 0f, 0f, null)
+ }
+
+ override fun hasOverlappingRendering(): Boolean = true
+
+}
diff --git a/core/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt b/core/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt
new file mode 100644
index 0000000..805fb21
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt
@@ -0,0 +1,140 @@
+package ca.allanwang.kau.views
+
+import android.animation.ArgbEvaluator
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import ca.allanwang.kau.utils.adjustAlpha
+
+/**
+ * Created by Allan Wang on 2016-11-17.
+ *
+ *
+ * Canvas drawn ripples that keep the previous color
+ * Extends to view dimensions
+ * Supports multiple ripples from varying locations
+ */
+class RippleCanvas @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+ private val paint: Paint = Paint()
+ private var baseColor = Color.TRANSPARENT
+ private val ripples: MutableList<Ripple> = mutableListOf()
+
+ init {
+ paint.isAntiAlias = true
+ paint.style = Paint.Style.FILL
+ }
+
+ /**
+ * Drawing the ripples involves having access to the next layer if it exists,
+ * and using its values to decide on the current color.
+ * If the next layer requests a fade, we will adjust the alpha of our current layer before drawing.
+ * Otherwise we will just draw the color as intended
+ */
+ override fun onDraw(canvas: Canvas) {
+ val itr = ripples.listIterator()
+ if (!itr.hasNext()) return canvas.drawColor(baseColor)
+ var next = itr.next()
+ canvas.drawColor(colorToDraw(baseColor, next.fade, next.radius, next.maxRadius))
+ var last = false
+ while (!last) {
+ val current = next
+ if (itr.hasNext()) next = itr.next()
+ else last = true
+ //We may fade any layer except for the last one
+ paint.color = colorToDraw(current.color, next.fade && !last, next.radius, next.maxRadius)
+ canvas.drawCircle(current.x, current.y, current.radius, paint)
+ if (current.radius == current.maxRadius) {
+ if (!last) {
+ itr.previous()
+ itr.remove()
+ itr.next()
+ } else {
+ itr.remove()
+ }
+ baseColor = current.color
+ }
+ }
+ }
+
+ /**
+ * Given our current color and next layer's radius & max,
+ * we will decide on the alpha of our current layer
+ */
+ internal fun colorToDraw(color: Int, fade: Boolean, current: Float, goal: Float): Int {
+ if (!fade || (current / goal <= FADE_PIVOT)) return color
+ val factor = (goal - current) / (goal - FADE_PIVOT * goal)
+ return color.adjustAlpha(factor)
+ }
+
+ /**
+ * Creates a ripple effect from the given starting values
+ * [fade] will gradually transition previous ripples to a transparent color so the resulting background is what we want
+ * this is typically only necessary if the ripple color has transparency
+ */
+ fun ripple(color: Int, startX: Float = 0f, startY: Float = 0f, duration: Long = 600L, fade: Boolean = Color.alpha(color) != 255) {
+ val w = width.toFloat()
+ val h = height.toFloat()
+ val x = when (startX) {
+ MIDDLE -> w / 2
+ END -> w
+ else -> startX
+ }
+ val y = when (startY) {
+ MIDDLE -> h / 2
+ END -> h
+ else -> startY
+ }
+ val maxRadius = Math.hypot(Math.max(x, w - x).toDouble(), Math.max(y, h - y).toDouble()).toFloat()
+ val ripple = Ripple(color, x, y, 0f, maxRadius, fade)
+ ripples.add(ripple)
+ val animator = ValueAnimator.ofFloat(0f, maxRadius)
+ animator.duration = duration
+ animator.addUpdateListener { animation ->
+ ripple.radius = animation.animatedValue as Float
+ invalidate()
+ }
+ animator.start()
+ }
+
+ /**
+ * Sets a color directly; clears ripple queue if it exists
+ */
+ fun set(color: Int) {
+ baseColor = color
+ ripples.clear()
+ invalidate()
+ }
+
+ /**
+ * Sets a color directly but with a transition
+ */
+ fun fade(color: Int, duration: Long = 300L) {
+ ripples.clear()
+ val animator = ValueAnimator.ofObject(ArgbEvaluator(), baseColor, color)
+ animator.duration = duration
+ animator.addUpdateListener { animation ->
+ baseColor = animation.animatedValue as Int
+ invalidate()
+ }
+ animator.start()
+ }
+
+ internal class Ripple(val color: Int,
+ val x: Float,
+ val y: Float,
+ var radius: Float,
+ val maxRadius: Float,
+ val fade: Boolean)
+
+ companion object {
+ const val MIDDLE = -1.0f
+ const val END = -2.0f
+ const val FADE_PIVOT = 0.5f
+ }
+}
diff --git a/core/src/main/kotlin/ca/allanwang/kau/views/SimpleRippleDrawable.kt b/core/src/main/kotlin/ca/allanwang/kau/views/SimpleRippleDrawable.kt
new file mode 100644
index 0000000..df842f6
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/views/SimpleRippleDrawable.kt
@@ -0,0 +1,19 @@
+package ca.allanwang.kau.views
+
+import android.content.res.ColorStateList
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.RippleDrawable
+import android.support.annotation.ColorInt
+import ca.allanwang.kau.utils.adjustAlpha
+
+/**
+ * Created by Allan Wang on 2017-06-24.
+ *
+ * Tries to mimic a standard ripple, given the foreground and background colors
+ */
+fun createSimpleRippleDrawable(@ColorInt foregroundColor: Int, @ColorInt backgroundColor: Int): RippleDrawable {
+ val states = ColorStateList(arrayOf(intArrayOf()), intArrayOf(foregroundColor))
+ val content = ColorDrawable(backgroundColor)
+ val mask = ColorDrawable(foregroundColor.adjustAlpha(0.16f))
+ return RippleDrawable(states, content, mask)
+} \ No newline at end of file
diff --git a/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt b/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt
new file mode 100644
index 0000000..38c99c3
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/widgets/ElasticDragDismissFrameLayout.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ca.allanwang.kau.widgets
+
+import android.app.Activity
+import android.content.Context
+import android.graphics.Color
+import android.util.AttributeSet
+import android.view.View
+import android.widget.FrameLayout
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.*
+
+/**
+ * A [FrameLayout] which responds to nested scrolls to create drag-dismissable layouts.
+ * Applies an elasticity factor to reduce movement as you approach the given dismiss distance.
+ * Optionally also scales down content during drag.
+ */
+class ElasticDragDismissFrameLayout @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
+) : FrameLayout(context, attrs, defStyleAttr, defStyleRes) {
+
+ // configurable attribs
+ private var dragDismissDistance = Float.MAX_VALUE
+ private var dragDismissFraction = -1f
+ private var dragDismissScale = 1f
+ private var shouldScale = false
+ private var dragElacticity = 0.8f
+
+ // state
+ private var totalDrag: Float = 0f
+ private var draggingDown = false
+ private var draggingUp = false
+
+ private var callbacks: MutableList<ElasticDragDismissCallback> = mutableListOf()
+
+ init {
+ if (attrs != null) {
+ val a = getContext().obtainStyledAttributes(attrs, R.styleable.ElasticDragDismissFrameLayout, 0, 0)
+ dragDismissDistance = a.getDimensionPixelSize(R.styleable.ElasticDragDismissFrameLayout_dragDismissDistance, Int.MAX_VALUE).toFloat()
+ dragDismissFraction = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissFraction, dragDismissFraction)
+ dragDismissScale = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragDismissScale, dragDismissScale)
+ shouldScale = dragDismissScale != 1f
+ dragElacticity = a.getFloat(R.styleable.ElasticDragDismissFrameLayout_dragElasticity, dragElacticity)
+ a.recycle()
+ }
+ }
+
+ abstract class ElasticDragDismissCallback {
+
+ /**
+ * Called for each drag event.
+
+ * @param elasticOffset Indicating the drag offset with elasticity applied i.e. may
+ * * exceed 1.
+ * *
+ * @param elasticOffsetPixels The elastically scaled drag distance in pixels.
+ * *
+ * @param rawOffset Value from [0, 1] indicating the raw drag offset i.e.
+ * * without elasticity applied. A value of 1 indicates that the
+ * * dismiss distance has been reached.
+ * *
+ * @param rawOffsetPixels The raw distance the user has dragged
+ */
+ internal open fun onDrag(elasticOffset: Float, elasticOffsetPixels: Float,
+ rawOffset: Float, rawOffsetPixels: Float) {
+ }
+
+ /**
+ * Called when dragging is released and has exceeded the threshold dismiss distance.
+ */
+ internal open fun onDragDismissed() {}
+
+ }
+
+ override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int): Boolean {
+ return nestedScrollAxes and View.SCROLL_AXIS_VERTICAL != 0
+ }
+
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
+ // if we're in a drag gesture and the user reverses up the we should take those events
+ if (draggingDown && dy > 0 || draggingUp && dy < 0) {
+ dragScale(dy)
+ consumed[1] = dy
+ }
+ }
+
+ override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int,
+ dxUnconsumed: Int, dyUnconsumed: Int) {
+ dragScale(dyUnconsumed)
+ }
+
+ override fun onStopNestedScroll(child: View) {
+ if (Math.abs(totalDrag) >= dragDismissDistance) {
+ dispatchDismissCallback()
+ } else { // settle back to natural position
+ animate()
+ .translationY(0f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(200L)
+ .setInterpolator(AnimHolder.fastOutSlowInInterpolator(context))
+ .setListener(null)
+ .start()
+ totalDrag = 0f
+ draggingUp = false
+ draggingDown = draggingUp
+ dispatchDragCallback(0f, 0f, 0f, 0f)
+ }
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ if (dragDismissFraction > 0f) {
+ dragDismissDistance = h * dragDismissFraction
+ }
+ }
+
+ fun addListener(listener: ElasticDragDismissCallback) {
+ callbacks.add(listener)
+ }
+
+ fun removeListener(listener: ElasticDragDismissCallback) {
+ callbacks.remove(listener)
+ }
+
+ private fun dragScale(scroll: Int) {
+ if (scroll == 0) return
+
+ totalDrag += scroll.toFloat()
+
+ // track the direction & set the pivot point for scaling
+ // don't double track i.e. if start dragging down and then reverse, keep tracking as
+ // dragging down until they reach the 'natural' position
+ if (scroll < 0 && !draggingUp && !draggingDown) {
+ draggingDown = true
+ if (shouldScale) pivotY = height.toFloat()
+ } else if (scroll > 0 && !draggingDown && !draggingUp) {
+ draggingUp = true
+ if (shouldScale) pivotY = 0f
+ }
+ // how far have we dragged relative to the distance to perform a dismiss
+ // (0–1 where 1 = dismiss distance). Decreasing logarithmically as we approach the limit
+ var dragFraction = Math.log10((1 + Math.abs(totalDrag) / dragDismissDistance).toDouble()).toFloat()
+
+ // calculate the desired translation given the drag fraction
+ var dragTo = dragFraction * dragDismissDistance * dragElacticity
+
+ if (draggingUp) {
+ // as we use the absolute magnitude when calculating the drag fraction, need to
+ // re-apply the drag direction
+ dragTo *= -1f
+ }
+ translationY = dragTo
+
+ if (shouldScale) {
+ val scale = 1 - (1 - dragDismissScale) * dragFraction
+ scaleX = scale
+ scaleY = scale
+ }
+
+ // if we've reversed direction and gone past the settle point then clear the flags to
+ // allow the list to get the scroll events & reset any transforms
+ if (draggingDown && totalDrag >= 0 || draggingUp && totalDrag <= 0) {
+ dragFraction = 0f
+ dragTo = dragFraction
+ totalDrag = dragTo
+ draggingUp = false
+ draggingDown = draggingUp
+ translationY = 0f
+ scaleX = 1f
+ scaleY = 1f
+ }
+ dispatchDragCallback(dragFraction, dragTo,
+ Math.min(1f, Math.abs(totalDrag) / dragDismissDistance), totalDrag)
+ }
+
+ private fun dispatchDragCallback(elasticOffset: Float, elasticOffsetPixels: Float,
+ rawOffset: Float, rawOffsetPixels: Float) {
+ callbacks.forEach {
+ it.onDrag(elasticOffset, elasticOffsetPixels,
+ rawOffset, rawOffsetPixels)
+ }
+ }
+
+ private fun dispatchDismissCallback() {
+ callbacks.forEach { it.onDragDismissed() }
+ }
+
+ /**
+ * An [ElasticDragDismissCallback] which fades system chrome (i.e. status bar and
+ * navigation bar) whilst elastic drags are performed and
+ * [finishes][Activity.finishAfterTransition] the activity when drag dismissed.
+ */
+ open class SystemChromeFader(private val activity: Activity) : ElasticDragDismissCallback() {
+ private val statusBarAlpha: Int = Color.alpha(activity.statusBarColor)
+ private val navBarAlpha: Int = Color.alpha(activity.navigationBarColor)
+ private val fadeNavBar: Boolean = activity.isNavBarOnBottom
+
+ public override fun onDrag(elasticOffset: Float, elasticOffsetPixels: Float,
+ rawOffset: Float, rawOffsetPixels: Float) {
+ if (elasticOffsetPixels > 0) {
+ // dragging downward, fade the status bar in proportion
+ activity.statusBarColor = activity.statusBarColor.withAlpha(((1f - rawOffset) * statusBarAlpha).toInt())
+ } else if (elasticOffsetPixels == 0f) {
+ // reset
+ activity.statusBarColor = activity.statusBarColor.withAlpha(statusBarAlpha)
+ activity.navigationBarColor = activity.navigationBarColor.withAlpha(navBarAlpha)
+ } else if (fadeNavBar) {
+ // dragging upward, fade the navigation bar in proportion
+ activity.navigationBarColor = activity.navigationBarColor.withAlpha(((1f - rawOffset) * navBarAlpha).toInt())
+ }
+ }
+
+ public override fun onDragDismissed() {
+ activity.finishAfterTransition()
+ }
+ }
+
+}
diff --git a/core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java b/core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java
new file mode 100644
index 0000000..78e915d
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/widgets/InkPageIndicator.java
@@ -0,0 +1,859 @@
+/*
+ * Copyright 2015 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package ca.allanwang.kau.widgets;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.support.annotation.ColorInt;
+import android.support.v4.view.ViewPager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.animation.Interpolator;
+
+import java.util.Arrays;
+
+import ca.allanwang.kau.R;
+import ca.allanwang.kau.utils.AnimHolder;
+import ca.allanwang.kau.utils.ColorUtilsKt;
+
+/**
+ * An ink inspired widget for indicating pages in a {@link ViewPager}.
+ */
+public class InkPageIndicator extends View implements ViewPager.OnPageChangeListener,
+ View.OnAttachStateChangeListener {
+
+ // defaults
+ private static final int DEFAULT_DOT_SIZE = 8; // dp
+ private static final int DEFAULT_GAP = 12; // dp
+ private static final int DEFAULT_ANIM_DURATION = 400; // ms
+ private static final int DEFAULT_UNSELECTED_COLOUR = 0x80ffffff; // 50% white
+ private static final int DEFAULT_SELECTED_COLOUR = 0xffffffff; // 100% white
+
+ // constants
+ private static final float INVALID_FRACTION = -1f;
+ private static final float MINIMAL_REVEAL = 0.00001f;
+
+ // configurable attributes
+ private int dotDiameter;
+ private int gap;
+ private long animDuration;
+ private int unselectedColour;
+ private int selectedColour;
+
+ public void setColour(@ColorInt int color) {
+ selectedColour = color;
+ unselectedColour = ColorUtilsKt.adjustAlpha(color, 0.5f);
+ selectedPaint.setColor(selectedColour);
+ unselectedPaint.setColor(unselectedColour);
+ }
+
+ // derived from attributes
+ private float dotRadius;
+ private float halfDotRadius;
+ private long animHalfDuration;
+ private float dotTopY;
+ private float dotCenterY;
+ private float dotBottomY;
+
+ // ViewPager
+ private ViewPager viewPager;
+
+ // state
+ private int pageCount;
+ private int currentPage;
+ private int previousPage;
+ private float selectedDotX;
+ private boolean selectedDotInPosition;
+ private float[] dotCenterX;
+ private float[] joiningFractions;
+ private float retreatingJoinX1;
+ private float retreatingJoinX2;
+ private float[] dotRevealFractions;
+ private boolean isAttachedToWindow;
+ private boolean pageChanging;
+
+ // drawing
+ private final Paint unselectedPaint;
+ private final Paint selectedPaint;
+ private final Path combinedUnselectedPath;
+ private final Path unselectedDotPath;
+ private final Path unselectedDotLeftPath;
+ private final Path unselectedDotRightPath;
+ private final RectF rectF;
+
+ // animation
+ private ValueAnimator moveAnimation;
+ private AnimatorSet joiningAnimationSet;
+ private PendingRetreatAnimator retreatAnimation;
+ private PendingRevealAnimator[] revealAnimations;
+ private final Interpolator interpolator;
+
+ // working values for beziers
+ float endX1;
+ float endY1;
+ float endX2;
+ float endY2;
+ float controlX1;
+ float controlY1;
+ float controlX2;
+ float controlY2;
+
+ public InkPageIndicator(Context context) {
+ this(context, null, 0);
+ }
+
+ public InkPageIndicator(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public InkPageIndicator(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final int density = (int) context.getResources().getDisplayMetrics().density;
+
+ // Load attributes
+ final TypedArray a = getContext().obtainStyledAttributes(
+ attrs, R.styleable.InkPageIndicator, defStyle, 0);
+
+ dotDiameter = a.getDimensionPixelSize(R.styleable.InkPageIndicator_dotDiameter,
+ DEFAULT_DOT_SIZE * density);
+ dotRadius = dotDiameter / 2;
+ halfDotRadius = dotRadius / 2;
+ gap = a.getDimensionPixelSize(R.styleable.InkPageIndicator_dotGap,
+ DEFAULT_GAP * density);
+ animDuration = (long) a.getInteger(R.styleable.InkPageIndicator_animationDuration,
+ DEFAULT_ANIM_DURATION);
+ animHalfDuration = animDuration / 2;
+ unselectedColour = a.getColor(R.styleable.InkPageIndicator_pageIndicatorColor,
+ DEFAULT_UNSELECTED_COLOUR);
+ selectedColour = a.getColor(R.styleable.InkPageIndicator_currentPageIndicatorColor,
+ DEFAULT_SELECTED_COLOUR);
+
+ a.recycle();
+
+ unselectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ unselectedPaint.setColor(unselectedColour);
+ selectedPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ selectedPaint.setColor(selectedColour);
+ interpolator = AnimHolder.INSTANCE.getFastOutSlowInInterpolator().invoke(context);
+
+ // create paths & rect now – reuse & rewind later
+ combinedUnselectedPath = new Path();
+ unselectedDotPath = new Path();
+ unselectedDotLeftPath = new Path();
+ unselectedDotRightPath = new Path();
+ rectF = new RectF();
+
+ addOnAttachStateChangeListener(this);
+ }
+
+ public void setViewPager(ViewPager viewPager) {
+ this.viewPager = viewPager;
+ viewPager.addOnPageChangeListener(this);
+ setPageCount(viewPager.getAdapter().getCount());
+ viewPager.getAdapter().registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ setPageCount(InkPageIndicator.this.viewPager.getAdapter().getCount());
+ }
+ });
+ setCurrentPageImmediate();
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ if (isAttachedToWindow) {
+ float fraction = positionOffset;
+ int currentPosition = pageChanging ? previousPage : currentPage;
+ int leftDotPosition = position;
+ // when swiping from #2 to #1 ViewPager reports position as 1 and a descending offset
+ // need to convert this into our left-dot-based 'coordinate space'
+ if (currentPosition != position) {
+ fraction = 1f - positionOffset;
+
+ // if user scrolls completely to next page then the position param updates to that
+ // new page but we're not ready to switch our 'current' page yet so adjust for that
+ if (fraction == 1f) {
+ leftDotPosition = Math.min(currentPosition, position);
+ }
+ }
+ setJoiningFraction(leftDotPosition, fraction);
+ }
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ if (isAttachedToWindow) {
+ // this is the main event we're interested in!
+ setSelectedPage(position);
+ } else {
+ // when not attached, don't animate the move, just store immediately
+ setCurrentPageImmediate();
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ // nothing to do
+ }
+
+ private void setPageCount(int pages) {
+ pageCount = pages;
+ resetState();
+ requestLayout();
+ }
+
+ private void calculateDotPositions(int width, int height) {
+ int left = getPaddingLeft();
+ int top = getPaddingTop();
+ int right = width - getPaddingRight();
+ int bottom = height - getPaddingBottom();
+
+ int requiredWidth = getRequiredWidth();
+ float startLeft = left + ((right - left - requiredWidth) / 2) + dotRadius;
+
+ dotCenterX = new float[pageCount];
+ for (int i = 0; i < pageCount; i++) {
+ dotCenterX[i] = startLeft + i * (dotDiameter + gap);
+ }
+ // todo just top aligning for now… should make this smarter
+ dotTopY = top;
+ dotCenterY = top + dotRadius;
+ dotBottomY = top + dotDiameter;
+
+ setCurrentPageImmediate();
+ }
+
+ private void setCurrentPageImmediate() {
+ if (viewPager != null) {
+ currentPage = viewPager.getCurrentItem();
+ } else {
+ currentPage = 0;
+ }
+ if (dotCenterX != null) {
+ selectedDotX = dotCenterX[currentPage];
+ }
+ }
+
+ private void resetState() {
+ joiningFractions = new float[pageCount - 1];
+ Arrays.fill(joiningFractions, 0f);
+ dotRevealFractions = new float[pageCount];
+ Arrays.fill(dotRevealFractions, 0f);
+ retreatingJoinX1 = INVALID_FRACTION;
+ retreatingJoinX2 = INVALID_FRACTION;
+ selectedDotInPosition = true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+ int desiredHeight = getDesiredHeight();
+ int height;
+ switch (MeasureSpec.getMode(heightMeasureSpec)) {
+ case MeasureSpec.EXACTLY:
+ height = MeasureSpec.getSize(heightMeasureSpec);
+ break;
+ case MeasureSpec.AT_MOST:
+ height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec));
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ height = desiredHeight;
+ break;
+ }
+
+ int desiredWidth = getDesiredWidth();
+ int width;
+ switch (MeasureSpec.getMode(widthMeasureSpec)) {
+ case MeasureSpec.EXACTLY:
+ width = MeasureSpec.getSize(widthMeasureSpec);
+ break;
+ case MeasureSpec.AT_MOST:
+ width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec));
+ break;
+ case MeasureSpec.UNSPECIFIED:
+ default:
+ width = desiredWidth;
+ break;
+ }
+ setMeasuredDimension(width, height);
+ calculateDotPositions(width, height);
+ }
+
+ private int getDesiredHeight() {
+ return getPaddingTop() + dotDiameter + getPaddingBottom();
+ }
+
+ private int getRequiredWidth() {
+ return pageCount * dotDiameter + (pageCount - 1) * gap;
+ }
+
+ private int getDesiredWidth() {
+ return getPaddingLeft() + getRequiredWidth() + getPaddingRight();
+ }
+
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ isAttachedToWindow = true;
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ isAttachedToWindow = false;
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (viewPager == null || pageCount == 0) return;
+ drawUnselected(canvas);
+ drawSelected(canvas);
+ }
+
+ private void drawUnselected(Canvas canvas) {
+
+ combinedUnselectedPath.rewind();
+
+ // draw any settled, revealing or joining dots
+ for (int page = 0; page < pageCount; page++) {
+ int nextXIndex = page == pageCount - 1 ? page : page + 1;
+ combinedUnselectedPath.op(getUnselectedPath(page,
+ dotCenterX[page],
+ dotCenterX[nextXIndex],
+ page == pageCount - 1 ? INVALID_FRACTION : joiningFractions[page],
+ dotRevealFractions[page]), Path.Op.UNION);
+ }
+ // draw any retreating joins
+ if (retreatingJoinX1 != INVALID_FRACTION) {
+ combinedUnselectedPath.op(getRetreatingJoinPath(), Path.Op.UNION);
+ }
+ canvas.drawPath(combinedUnselectedPath, unselectedPaint);
+ }
+
+ /**
+ * Unselected dots can be in 6 states:
+ * <p>
+ * #1 At rest
+ * #2 Joining neighbour, still separate
+ * #3 Joining neighbour, combined curved
+ * #4 Joining neighbour, combined straight
+ * #5 Join retreating
+ * #6 Dot re-showing / revealing
+ * <p>
+ * It can also be in a combination of these states e.g. joining one neighbour while
+ * retreating from another. We therefore create a Path so that we can examine each
+ * dot pair separately and later take the union for these cases.
+ * <p>
+ * This function returns a path for the given dot **and any action to it's right** e.g. joining
+ * or retreating from it's neighbour
+ *
+ * @param page
+ * @return
+ */
+ private Path getUnselectedPath(int page,
+ float centerX,
+ float nextCenterX,
+ float joiningFraction,
+ float dotRevealFraction) {
+
+ unselectedDotPath.rewind();
+
+ if ((joiningFraction == 0f || joiningFraction == INVALID_FRACTION)
+ && dotRevealFraction == 0f
+ && !(page == currentPage && selectedDotInPosition == true)) {
+
+ // case #1 – At rest
+ unselectedDotPath.addCircle(dotCenterX[page], dotCenterY, dotRadius, Path.Direction.CW);
+ }
+
+ if (joiningFraction > 0f && joiningFraction <= 0.5f
+ && retreatingJoinX1 == INVALID_FRACTION) {
+
+ // case #2 – Joining neighbour, still separate
+
+ // start with the left dot
+ unselectedDotLeftPath.rewind();
+
+ // start at the bottom center
+ unselectedDotLeftPath.moveTo(centerX, dotBottomY);
+
+ // semi circle to the top center
+ rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
+ unselectedDotLeftPath.arcTo(rectF, 90, 180, true);
+
+ // cubic to the right middle
+ endX1 = centerX + dotRadius + (joiningFraction * gap);
+ endY1 = dotCenterY;
+ controlX1 = centerX + halfDotRadius;
+ controlY1 = dotTopY;
+ controlX2 = endX1;
+ controlY2 = endY1 - halfDotRadius;
+ unselectedDotLeftPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX1, endY1);
+
+ // cubic back to the bottom center
+ endX2 = centerX;
+ endY2 = dotBottomY;
+ controlX1 = endX1;
+ controlY1 = endY1 + halfDotRadius;
+ controlX2 = centerX + halfDotRadius;
+ controlY2 = dotBottomY;
+ unselectedDotLeftPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX2, endY2);
+
+ unselectedDotPath.op(unselectedDotLeftPath, Path.Op.UNION);
+
+ // now do the next dot to the right
+ unselectedDotRightPath.rewind();
+
+ // start at the bottom center
+ unselectedDotRightPath.moveTo(nextCenterX, dotBottomY);
+
+ // semi circle to the top center
+ rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
+ unselectedDotRightPath.arcTo(rectF, 90, -180, true);
+
+ // cubic to the left middle
+ endX1 = nextCenterX - dotRadius - (joiningFraction * gap);
+ endY1 = dotCenterY;
+ controlX1 = nextCenterX - halfDotRadius;
+ controlY1 = dotTopY;
+ controlX2 = endX1;
+ controlY2 = endY1 - halfDotRadius;
+ unselectedDotRightPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX1, endY1);
+
+ // cubic back to the bottom center
+ endX2 = nextCenterX;
+ endY2 = dotBottomY;
+ controlX1 = endX1;
+ controlY1 = endY1 + halfDotRadius;
+ controlX2 = endX2 - halfDotRadius;
+ controlY2 = dotBottomY;
+ unselectedDotRightPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX2, endY2);
+ unselectedDotPath.op(unselectedDotRightPath, Path.Op.UNION);
+ }
+
+ if (joiningFraction > 0.5f && joiningFraction < 1f
+ && retreatingJoinX1 == INVALID_FRACTION) {
+
+ // case #3 – Joining neighbour, combined curved
+
+ // adjust the fraction so that it goes from 0.3 -> 1 to produce a more realistic 'join'
+ float adjustedFraction = (joiningFraction - 0.2f) * 1.25f;
+
+ // start in the bottom left
+ unselectedDotPath.moveTo(centerX, dotBottomY);
+
+ // semi-circle to the top left
+ rectF.set(centerX - dotRadius, dotTopY, centerX + dotRadius, dotBottomY);
+ unselectedDotPath.arcTo(rectF, 90, 180, true);
+
+ // bezier to the middle top of the join
+ endX1 = centerX + dotRadius + (gap / 2);
+ endY1 = dotCenterY - (adjustedFraction * dotRadius);
+ controlX1 = endX1 - (adjustedFraction * dotRadius);
+ controlY1 = dotTopY;
+ controlX2 = endX1 - ((1 - adjustedFraction) * dotRadius);
+ controlY2 = endY1;
+ unselectedDotPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX1, endY1);
+
+ // bezier to the top right of the join
+ endX2 = nextCenterX;
+ endY2 = dotTopY;
+ controlX1 = endX1 + ((1 - adjustedFraction) * dotRadius);
+ controlY1 = endY1;
+ controlX2 = endX1 + (adjustedFraction * dotRadius);
+ controlY2 = dotTopY;
+ unselectedDotPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX2, endY2);
+
+ // semi-circle to the bottom right
+ rectF.set(nextCenterX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
+ unselectedDotPath.arcTo(rectF, 270, 180, true);
+
+ // bezier to the middle bottom of the join
+ // endX1 stays the same
+ endY1 = dotCenterY + (adjustedFraction * dotRadius);
+ controlX1 = endX1 + (adjustedFraction * dotRadius);
+ controlY1 = dotBottomY;
+ controlX2 = endX1 + ((1 - adjustedFraction) * dotRadius);
+ controlY2 = endY1;
+ unselectedDotPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX1, endY1);
+
+ // bezier back to the start point in the bottom left
+ endX2 = centerX;
+ endY2 = dotBottomY;
+ controlX1 = endX1 - ((1 - adjustedFraction) * dotRadius);
+ controlY1 = endY1;
+ controlX2 = endX1 - (adjustedFraction * dotRadius);
+ controlY2 = endY2;
+ unselectedDotPath.cubicTo(controlX1, controlY1,
+ controlX2, controlY2,
+ endX2, endY2);
+ }
+ if (joiningFraction == 1 && retreatingJoinX1 == INVALID_FRACTION) {
+
+ // case #4 Joining neighbour, combined straight technically we could use case 3 for this
+ // situation as well but assume that this is an optimization rather than faffing around
+ // with beziers just to draw a rounded rect
+ rectF.set(centerX - dotRadius, dotTopY, nextCenterX + dotRadius, dotBottomY);
+ unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
+ }
+
+ // case #5 is handled by #getRetreatingJoinPath()
+ // this is done separately so that we can have a single retreating path spanning
+ // multiple dots and therefore animate it's movement smoothly
+
+ if (dotRevealFraction > MINIMAL_REVEAL) {
+
+ // case #6 – previously hidden dot revealing
+ unselectedDotPath.addCircle(centerX, dotCenterY, dotRevealFraction * dotRadius,
+ Path.Direction.CW);
+ }
+
+ return unselectedDotPath;
+ }
+
+ private Path getRetreatingJoinPath() {
+ unselectedDotPath.rewind();
+ rectF.set(retreatingJoinX1, dotTopY, retreatingJoinX2, dotBottomY);
+ unselectedDotPath.addRoundRect(rectF, dotRadius, dotRadius, Path.Direction.CW);
+ return unselectedDotPath;
+ }
+
+ private void drawSelected(Canvas canvas) {
+ canvas.drawCircle(selectedDotX, dotCenterY, dotRadius, selectedPaint);
+ }
+
+ private void setSelectedPage(int now) {
+ if (now == currentPage) return;
+
+ pageChanging = true;
+ previousPage = currentPage;
+ currentPage = now;
+ final int steps = Math.abs(now - previousPage);
+
+ if (steps > 1) {
+ if (now > previousPage) {
+ for (int i = 0; i < steps; i++) {
+ setJoiningFraction(previousPage + i, 1f);
+ }
+ } else {
+ for (int i = -1; i > -steps; i--) {
+ setJoiningFraction(previousPage + i, 1f);
+ }
+ }
+ }
+
+ // create the anim to move the selected dot – this animator will kick off
+ // retreat animations when it has moved 75% of the way.
+ // The retreat animation in turn will kick of reveal anims when the
+ // retreat has passed any dots to be revealed
+ moveAnimation = createMoveSelectedAnimator(dotCenterX[now], previousPage, now, steps);
+ moveAnimation.start();
+ }
+
+ private ValueAnimator createMoveSelectedAnimator(
+ final float moveTo, int was, int now, int steps) {
+
+ // create the actual move animator
+ ValueAnimator moveSelected = ValueAnimator.ofFloat(selectedDotX, moveTo);
+
+ // also set up a pending retreat anim – this starts when the move is 75% complete
+ retreatAnimation = new PendingRetreatAnimator(was, now, steps,
+ now > was ?
+ new RightwardStartPredicate(moveTo - ((moveTo - selectedDotX) * 0.25f)) :
+ new LeftwardStartPredicate(moveTo + ((selectedDotX - moveTo) * 0.25f)));
+ retreatAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ resetState();
+ pageChanging = false;
+ }
+ });
+ moveSelected.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ // todo avoid autoboxing
+ selectedDotX = (Float) valueAnimator.getAnimatedValue();
+ retreatAnimation.startIfNecessary(selectedDotX);
+ postInvalidateOnAnimation();
+ }
+ });
+ moveSelected.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ // set a flag so that we continue to draw the unselected dot in the target position
+ // until the selected dot has finished moving into place
+ selectedDotInPosition = false;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ // set a flag when anim finishes so that we don't draw both selected & unselected
+ // page dots
+ selectedDotInPosition = true;
+ }
+ });
+ // slightly delay the start to give the joins a chance to run
+ // unless dot isn't in position yet – then don't delay!
+ moveSelected.setStartDelay(selectedDotInPosition ? animDuration / 4l : 0l);
+ moveSelected.setDuration(animDuration * 3l / 4l);
+ moveSelected.setInterpolator(interpolator);
+ return moveSelected;
+ }
+
+ private void setJoiningFraction(int leftDot, float fraction) {
+ if (leftDot < joiningFractions.length) {
+
+ if (leftDot == 1) {
+ Log.d("PageIndicator", "dot 1 fraction:\t" + fraction);
+ }
+
+ joiningFractions[leftDot] = fraction;
+ postInvalidateOnAnimation();
+ }
+ }
+
+ private void clearJoiningFractions() {
+ Arrays.fill(joiningFractions, 0f);
+ postInvalidateOnAnimation();
+ }
+
+ private void setDotRevealFraction(int dot, float fraction) {
+ dotRevealFractions[dot] = fraction;
+ postInvalidateOnAnimation();
+ }
+
+ private void cancelJoiningAnimations() {
+ if (joiningAnimationSet != null && joiningAnimationSet.isRunning()) {
+ joiningAnimationSet.cancel();
+ }
+ }
+
+ /**
+ * A {@link ValueAnimator} that starts once a given predicate returns true.
+ */
+ public abstract class PendingStartAnimator extends ValueAnimator {
+
+ protected boolean hasStarted;
+ protected StartPredicate predicate;
+
+ public PendingStartAnimator(StartPredicate predicate) {
+ super();
+ this.predicate = predicate;
+ hasStarted = false;
+ }
+
+ public void startIfNecessary(float currentValue) {
+ if (!hasStarted && predicate.shouldStart(currentValue)) {
+ start();
+ hasStarted = true;
+ }
+ }
+ }
+
+ /**
+ * An Animator that shows and then shrinks a retreating join between the previous and newly
+ * selected pages. This also sets up some pending dot reveals – to be started when the retreat
+ * has passed the dot to be revealed.
+ */
+ public class PendingRetreatAnimator extends PendingStartAnimator {
+
+ public PendingRetreatAnimator(int was, int now, int steps, StartPredicate predicate) {
+ super(predicate);
+ setDuration(animHalfDuration);
+ setInterpolator(interpolator);
+
+ // work out the start/end values of the retreating join from the direction we're
+ // travelling in. Also look at the current selected dot position, i.e. we're moving on
+ // before a prior anim has finished.
+ final float initialX1 = now > was ? Math.min(dotCenterX[was], selectedDotX) - dotRadius
+ : dotCenterX[now] - dotRadius;
+ final float finalX1 = now > was ? dotCenterX[now] - dotRadius
+ : dotCenterX[now] - dotRadius;
+ final float initialX2 = now > was ? dotCenterX[now] + dotRadius
+ : Math.max(dotCenterX[was], selectedDotX) + dotRadius;
+ final float finalX2 = now > was ? dotCenterX[now] + dotRadius
+ : dotCenterX[now] + dotRadius;
+
+ revealAnimations = new PendingRevealAnimator[steps];
+ // hold on to the indexes of the dots that will be hidden by the retreat so that
+ // we can initialize their revealFraction's i.e. make sure they're hidden while the
+ // reveal animation runs
+ final int[] dotsToHide = new int[steps];
+ if (initialX1 != finalX1) { // rightward retreat
+ setFloatValues(initialX1, finalX1);
+ // create the reveal animations that will run when the retreat passes them
+ for (int i = 0; i < steps; i++) {
+ revealAnimations[i] = new PendingRevealAnimator(was + i,
+ new RightwardStartPredicate(dotCenterX[was + i]));
+ dotsToHide[i] = was + i;
+ }
+ addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ // todo avoid autoboxing
+ retreatingJoinX1 = (Float) valueAnimator.getAnimatedValue();
+ postInvalidateOnAnimation();
+ // start any reveal animations if we've passed them
+ for (PendingRevealAnimator pendingReveal : revealAnimations) {
+ pendingReveal.startIfNecessary(retreatingJoinX1);
+ }
+ }
+ });
+ } else { // (initialX2 != finalX2) leftward retreat
+ setFloatValues(initialX2, finalX2);
+ // create the reveal animations that will run when the retreat passes them
+ for (int i = 0; i < steps; i++) {
+ revealAnimations[i] = new PendingRevealAnimator(was - i,
+ new LeftwardStartPredicate(dotCenterX[was - i]));
+ dotsToHide[i] = was - i;
+ }
+ addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ // todo avoid autoboxing
+ retreatingJoinX2 = (Float) valueAnimator.getAnimatedValue();
+ postInvalidateOnAnimation();
+ // start any reveal animations if we've passed them
+ for (PendingRevealAnimator pendingReveal : revealAnimations) {
+ pendingReveal.startIfNecessary(retreatingJoinX2);
+ }
+ }
+ });
+ }
+
+ addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ cancelJoiningAnimations();
+ clearJoiningFractions();
+ // we need to set this so that the dots are hidden until the reveal anim runs
+ for (int dot : dotsToHide) {
+ setDotRevealFraction(dot, MINIMAL_REVEAL);
+ }
+ retreatingJoinX1 = initialX1;
+ retreatingJoinX2 = initialX2;
+ postInvalidateOnAnimation();
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ retreatingJoinX1 = INVALID_FRACTION;
+ retreatingJoinX2 = INVALID_FRACTION;
+ postInvalidateOnAnimation();
+ }
+ });
+ }
+ }
+
+ /**
+ * An Animator that animates a given dot's revealFraction i.e. scales it up
+ */
+ public class PendingRevealAnimator extends PendingStartAnimator {
+
+ private int dot;
+
+ public PendingRevealAnimator(int dot, StartPredicate predicate) {
+ super(predicate);
+ setFloatValues(MINIMAL_REVEAL, 1f);
+ this.dot = dot;
+ setDuration(animHalfDuration);
+ setInterpolator(interpolator);
+ addUpdateListener(new AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ // todo avoid autoboxing
+ setDotRevealFraction(PendingRevealAnimator.this.dot,
+ (Float) valueAnimator.getAnimatedValue());
+ }
+ });
+ addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ setDotRevealFraction(PendingRevealAnimator.this.dot, 0f);
+ postInvalidateOnAnimation();
+ }
+ });
+ }
+ }
+
+ /**
+ * A predicate used to start an animation when a test passes
+ */
+ public abstract class StartPredicate {
+
+ protected float thresholdValue;
+
+ public StartPredicate(float thresholdValue) {
+ this.thresholdValue = thresholdValue;
+ }
+
+ abstract boolean shouldStart(float currentValue);
+
+ }
+
+ /**
+ * A predicate used to start an animation when a given value is greater than a threshold
+ */
+ public class RightwardStartPredicate extends StartPredicate {
+
+ public RightwardStartPredicate(float thresholdValue) {
+ super(thresholdValue);
+ }
+
+ boolean shouldStart(float currentValue) {
+ return currentValue > thresholdValue;
+ }
+ }
+
+ /**
+ * A predicate used to start an animation then a given value is less than a threshold
+ */
+ public class LeftwardStartPredicate extends StartPredicate {
+
+ public LeftwardStartPredicate(float thresholdValue) {
+ super(thresholdValue);
+ }
+
+ boolean shouldStart(float currentValue) {
+ return currentValue < thresholdValue;
+ }
+ }
+}
diff --git a/core/src/main/kotlin/ca/allanwang/kau/widgets/TextSlider.kt b/core/src/main/kotlin/ca/allanwang/kau/widgets/TextSlider.kt
new file mode 100644
index 0000000..528dabc
--- /dev/null
+++ b/core/src/main/kotlin/ca/allanwang/kau/widgets/TextSlider.kt
@@ -0,0 +1,125 @@
+package ca.allanwang.kau.widgets
+
+import android.content.Context
+import android.graphics.Color
+import android.support.v4.widget.TextViewCompat
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.Gravity
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.widget.TextSwitcher
+import android.widget.TextView
+import ca.allanwang.kau.R
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-21.
+ *
+ * Text switcher with global text color and embedded sliding animations
+ * Also has a stack to keep track of title changes
+ */
+class TextSlider @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null
+) : TextSwitcher(context, attrs) {
+
+ val titleStack: Stack<CharSequence?> = Stack()
+
+ /**
+ * Holds a mapping of animation types to their respective animations
+ */
+ val animationMap = mapOf(
+ ANIMATION_NONE to null,
+ ANIMATION_SLIDE_HORIZONTAL to AnimationBundle(
+ R.anim.kau_slide_in_right, R.anim.kau_slide_out_left,
+ R.anim.kau_slide_in_left, R.anim.kau_slide_out_right),
+ ANIMATION_SLIDE_VERTICAL to AnimationBundle(
+ R.anim.kau_slide_in_bottom, R.anim.kau_slide_out_top,
+ R.anim.kau_slide_in_top, R.anim.kau_slide_out_bottom
+ )
+ )
+
+ /**
+ * Holds lazy instances of the animations
+ */
+ inner class AnimationBundle(private val nextIn: Int, private val nextOut: Int, private val prevIn: Int, private val prevOut: Int) {
+ val NEXT_IN: Animation by lazy { AnimationUtils.loadAnimation(context, nextIn) }
+ val NEXT_OUT: Animation by lazy { AnimationUtils.loadAnimation(context, nextOut) }
+ val PREV_IN: Animation by lazy { AnimationUtils.loadAnimation(context, prevIn) }
+ val PREV_OUT: Animation by lazy { AnimationUtils.loadAnimation(context, prevOut) }
+ }
+
+ companion object {
+ const val ANIMATION_NONE = 1000
+ const val ANIMATION_SLIDE_HORIZONTAL = 1001
+ const val ANIMATION_SLIDE_VERTICAL = 1002
+ }
+
+ var animationType: Int = ANIMATION_SLIDE_HORIZONTAL
+
+ var textColor: Int = Color.WHITE
+ get() = field
+ set(value) {
+ field = value
+ (getChildAt(0) as TextView).setTextColor(value)
+ (getChildAt(1) as TextView).setTextColor(value)
+ }
+ val isRoot: Boolean
+ get() = titleStack.size <= 1
+
+ init {
+ if (attrs != null) {
+ val styledAttrs = context.obtainStyledAttributes(attrs, R.styleable.TextSlider)
+ animationType = styledAttrs.getInteger(R.styleable.TextSlider_animation_type, ANIMATION_SLIDE_HORIZONTAL)
+ styledAttrs.recycle()
+ }
+ }
+
+ override fun setText(text: CharSequence?) {
+ if ((currentView as TextView).text == text) return
+ super.setText(text)
+ }
+
+ override fun setCurrentText(text: CharSequence?) {
+ if (titleStack.isNotEmpty()) titleStack.pop()
+ titleStack.push(text)
+ super.setCurrentText(text)
+ }
+
+ fun setNextText(text: CharSequence?) {
+ if (titleStack.isEmpty()) {
+ setCurrentText(text)
+ return
+ }
+ titleStack.push(text)
+ val anim = animationMap[animationType]
+ inAnimation = anim?.NEXT_IN
+ outAnimation = anim?.NEXT_OUT
+ setText(text)
+ }
+
+ /**
+ * Sets the text as the previous title
+ * No further checks are done, so be sure to verify with [isRoot]
+ */
+ @Throws(EmptyStackException::class)
+ fun setPrevText() {
+ titleStack.pop()
+ val anim = animationMap[animationType]
+ inAnimation = anim?.PREV_IN
+ outAnimation = anim?.PREV_OUT
+ val text = titleStack.peek()
+ setText(text)
+ }
+
+ init {
+ setFactory {
+ TextView(context).apply {
+ //replica of toolbar title
+ gravity = Gravity.START
+ setSingleLine()
+ ellipsize = TextUtils.TruncateAt.END
+ TextViewCompat.setTextAppearance(this, R.style.TextAppearance_AppCompat_Title)
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_fade_in.xml b/core/src/main/res/anim/kau_fade_in.xml
new file mode 100644
index 0000000..03312a2
--- /dev/null
+++ b/core/src/main/res/anim/kau_fade_in.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromAlpha="0.0"
+ android:toAlpha="1.0" /> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_fade_out.xml b/core/src/main/res/anim/kau_fade_out.xml
new file mode 100644
index 0000000..322befc
--- /dev/null
+++ b/core/src/main/res/anim/kau_fade_out.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<alpha xmlns:android="http://schemas.android.com/apk/res/android"
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromAlpha="1.0"
+ android:toAlpha="0.0" /> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_in_bottom.xml b/core/src/main/res/anim/kau_slide_in_bottom.xml
new file mode 100644
index 0000000..a07cba4
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_in_bottom.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromYDelta="100%p"
+ android:toYDelta="0" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_in_left.xml b/core/src/main/res/anim/kau_slide_in_left.xml
new file mode 100644
index 0000000..828bdae
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_in_left.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromXDelta="-100%p"
+ android:toXDelta="0" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_in_right.xml b/core/src/main/res/anim/kau_slide_in_right.xml
new file mode 100644
index 0000000..0f9fbce
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_in_right.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromXDelta="100%p"
+ android:toXDelta="0" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_in_top.xml b/core/src/main/res/anim/kau_slide_in_top.xml
new file mode 100644
index 0000000..8caee9b
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_in_top.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromYDelta="-100%p"
+ android:toYDelta="0" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_out_bottom.xml b/core/src/main/res/anim/kau_slide_out_bottom.xml
new file mode 100644
index 0000000..44e7e21
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_out_bottom.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromYDelta="0"
+ android:toYDelta="100%p" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_out_left.xml b/core/src/main/res/anim/kau_slide_out_left.xml
new file mode 100644
index 0000000..6fab33e
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_out_left.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromXDelta="0"
+ android:toXDelta="-100%p" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_out_right.xml b/core/src/main/res/anim/kau_slide_out_right.xml
new file mode 100644
index 0000000..9322741
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_out_right.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromXDelta="0"
+ android:toXDelta="100%p" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_out_right_top.xml b/core/src/main/res/anim/kau_slide_out_right_top.xml
new file mode 100644
index 0000000..5b63474
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_out_right_top.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android"
+ android:zAdjustment="top">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromXDelta="0"
+ android:toXDelta="100%p" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/anim/kau_slide_out_top.xml b/core/src/main/res/anim/kau_slide_out_top.xml
new file mode 100644
index 0000000..9f8e14c
--- /dev/null
+++ b/core/src/main/res/anim/kau_slide_out_top.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate
+ android:duration="@android:integer/config_shortAnimTime"
+ android:fromYDelta="0"
+ android:toYDelta="-100%p" />
+</set> \ No newline at end of file
diff --git a/core/src/main/res/drawable/kau_transparent.xml b/core/src/main/res/drawable/kau_transparent.xml
new file mode 100644
index 0000000..399a46b
--- /dev/null
+++ b/core/src/main/res/drawable/kau_transparent.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="@android:color/transparent" />
+</shape> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_about_section_libraries.xml b/core/src/main/res/layout/kau_about_section_libraries.xml
new file mode 100644
index 0000000..45c271b
--- /dev/null
+++ b/core/src/main/res/layout/kau_about_section_libraries.xml
@@ -0,0 +1,27 @@
+<?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/about_library_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/about_library_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="@dimen/activity_horizontal_margin"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="0.5"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/about_library_recycler"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/about_library_title" />
+
+</android.support.constraint.ConstraintLayout>
diff --git a/core/src/main/res/layout/kau_about_section_main.xml b/core/src/main/res/layout/kau_about_section_main.xml
new file mode 100644
index 0000000..40d8dfb
--- /dev/null
+++ b/core/src/main/res/layout/kau_about_section_main.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v4.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:showIn="@layout/kau_activity_about">
+
+ <android.support.constraint.ConstraintLayout
+ android:id="@+id/about_main_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ca.allanwang.kau.views.CutoutView
+ android:id="@+id/about_main_cutout"
+ android:layout_width="0dp"
+ android:layout_height="@dimen/kau_about_header_height"
+ android:minHeight="@dimen/kau_about_header_height"
+ app:foregroundColor="?android:colorAccent"
+ app:heightPercentageToScreen="0.5"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <FrameLayout
+ android:id="@+id/about_main_bottom_container"
+ android:layout_width="0dp"
+ android:layout_height="2000dp"
+ android:background="?android:attr/colorBackground"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/about_main_cutout">
+
+ <TextView
+ android:id="@+id/about_main_bottom_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:layout_margin="@dimen/activity_horizontal_margin" />
+
+ </FrameLayout>
+
+ </android.support.constraint.ConstraintLayout>
+
+</android.support.v4.widget.NestedScrollView>
diff --git a/core/src/main/res/layout/kau_activity_about.xml b/core/src/main/res/layout/kau_activity_about.xml
new file mode 100644
index 0000000..33cc781
--- /dev/null
+++ b/core/src/main/res/layout/kau_activity_about.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ca.allanwang.kau.widgets.ElasticDragDismissFrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/about_draggable_frame"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ app:dragDismissDistance="@dimen/kau_drag_dismiss_distance"
+ app:dragDismissScale="0.95"
+ tools:context=".about.AboutActivityBase">
+
+ <android.support.v4.view.ViewPager
+ android:id="@+id/about_pager"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <ca.allanwang.kau.widgets.InkPageIndicator
+ android:id="@+id/about_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|center_horizontal"
+ android:layout_marginTop="@dimen/kau_padding_normal"
+ app:pageIndicatorColor="@color/kau_about_page_indicator_dark"
+ app:currentPageIndicatorColor="@color/kau_about_page_indicator_dark_selected" />
+
+</ca.allanwang.kau.widgets.ElasticDragDismissFrameLayout>
diff --git a/core/src/main/res/layout/kau_activity_kpref.xml b/core/src/main/res/layout/kau_activity_kpref.xml
new file mode 100644
index 0000000..05cacb5
--- /dev/null
+++ b/core/src/main/res/layout/kau_activity_kpref.xml
@@ -0,0 +1,54 @@
+<?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_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <ca.allanwang.kau.views.RippleCanvas
+ android:id="@+id/kau_toolbar_ripple"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="@+id/kau_toolbar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <android.support.v7.widget.Toolbar
+ android:id="@id/kau_toolbar"
+ android:layout_width="0dp"
+ android:layout_height="?attr/actionBarSize"
+ android:layout_marginTop="@dimen/kau_status_bar_height"
+ android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ca.allanwang.kau.widgets.TextSlider
+ android:id="@+id/kau_toolbar_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ app:animation_type="slide_horizontal" />
+
+ </android.support.v7.widget.Toolbar>
+
+ <ca.allanwang.kau.views.RippleCanvas
+ android:id="@+id/kau_ripple"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/kau_toolbar" />
+
+ <ViewAnimator
+ android:id="@+id/kau_holder"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/kau_toolbar" />
+
+</android.support.constraint.ConstraintLayout>
diff --git a/core/src/main/res/layout/kau_changelog_content.xml b/core/src/main/res/layout/kau_changelog_content.xml
new file mode 100644
index 0000000..92b87b9
--- /dev/null
+++ b/core/src/main/res/layout/kau_changelog_content.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingBottom="8.4sp"
+ android:paddingLeft="@dimen/kau_dialog_margin"
+ android:paddingRight="@dimen/kau_dialog_margin">
+
+ <!--padding bottom is 14sp * 0.6-->
+
+ <TextView
+ android:id="@+id/kau_changelog_bullet"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lineSpacingMultiplier="1.6"
+ android:paddingRight="5dp"
+ android:text="@string/kau_u2022"
+ android:textAppearance="@style/TextAppearance.AppCompat.Small" />
+
+ <TextView
+ android:id="@+id/kau_changelog_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lineSpacingMultiplier="1.6"
+ android:textAppearance="@style/TextAppearance.AppCompat.Small" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_changelog_title.xml b/core/src/main/res/layout/kau_changelog_title.xml
new file mode 100644
index 0000000..e885e02
--- /dev/null
+++ b/core/src/main/res/layout/kau_changelog_title.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_changelog_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lineSpacingMultiplier="1.6"
+ android:paddingBottom="@dimen/kau_dialog_margin_bottom"
+ android:paddingLeft="@dimen/kau_dialog_margin"
+ android:paddingRight="@dimen/kau_dialog_margin"
+ android:paddingTop="@dimen/kau_dialog_margin_bottom"
+ android:textAppearance="@style/TextAppearance.AppCompat.Medium"
+ android:textStyle="bold" />
+
diff --git a/core/src/main/res/layout/kau_iitem_card.xml b/core/src/main/res/layout/kau_iitem_card.xml
new file mode 100644
index 0000000..621da2e
--- /dev/null
+++ b/core/src/main/res/layout/kau_iitem_card.xml
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Generic card with an imageview, title, description, and button
+ -->
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/kau_card_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/kau_padding_normal"
+ android:background="?android:selectableItemBackground"
+ android:minHeight="?android:listPreferredItemHeight">
+
+ <android.support.constraint.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/kau_padding_normal"
+ android:paddingEnd="@dimen/kau_padding_normal"
+ android:paddingTop="@dimen/kau_padding_normal">
+
+ <ImageView
+ android:id="@+id/kau_card_image"
+ android:layout_width="@dimen/kau_avatar_bounds"
+ android:layout_height="@dimen/kau_avatar_bounds"
+ android:layout_marginEnd="@dimen/kau_avatar_margin"
+ android:layout_marginStart="@dimen/kau_avatar_margin"
+ android:padding="@dimen/kau_avatar_padding"
+ android:scaleType="fitCenter"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+
+ <TextView
+ android:id="@+id/kau_card_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_goneMarginStart="@dimen/kau_padding_normal" />
+
+ <TextView
+ android:id="@+id/kau_card_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toBottomOf="@id/kau_card_title"
+ app:layout_goneMarginStart="@dimen/kau_padding_normal" />
+
+ <LinearLayout
+ android:id="@+id/kau_card_bottom_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:visibility="gone"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/kau_card_image"
+ app:layout_constraintTop_toBottomOf="@id/kau_card_description"
+ app:layout_goneMarginStart="@dimen/kau_padding_normal">
+
+ <Button
+ android:id="@+id/kau_card_button"
+ style="?android:borderlessButtonStyle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/kau_spacing_normal"
+ android:textColor="?attr/colorAccent" />
+
+ </LinearLayout>
+
+ </android.support.constraint.ConstraintLayout>
+
+</android.support.v7.widget.CardView>
diff --git a/core/src/main/res/layout/kau_iitem_cutout.xml b/core/src/main/res/layout/kau_iitem_cutout.xml
new file mode 100644
index 0000000..a4b17ac
--- /dev/null
+++ b/core/src/main/res/layout/kau_iitem_cutout.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ca.allanwang.kau.views.CutoutView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/kau_cutout"
+ android:layout_width="match_parent"
+ android:layout_height="@dimen/kau_about_header_height"
+ android:minHeight="@dimen/kau_about_header_height"
+ app:foregroundColor="?android:colorAccent"
+ app:heightPercentageToScreen="0.5" /> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_iitem_header.xml b/core/src/main/res/layout/kau_iitem_header.xml
new file mode 100644
index 0000000..fa5a595
--- /dev/null
+++ b/core/src/main/res/layout/kau_iitem_header.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_header_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/kau_padding_normal"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/kau_header_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/kau_spacing_xlarge"
+ android:padding="@dimen/kau_padding_normal"
+ android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
+
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_iitem_library.xml b/core/src/main/res/layout/kau_iitem_library.xml
new file mode 100644
index 0000000..1c3de5c
--- /dev/null
+++ b/core/src/main/res/layout/kau_iitem_library.xml
@@ -0,0 +1,124 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:id="@+id/lib_item_card"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/kau_padding_normal"
+ android:background="?android:selectableItemBackground"
+ android:clickable="true">
+
+ <android.support.constraint.ConstraintLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingBottom="@dimen/kau_padding_normal"
+ android:paddingEnd="@dimen/kau_padding_normal"
+ android:paddingStart="@dimen/kau_padding_normal"
+ android:paddingTop="@dimen/kau_padding_normal">
+
+ <android.support.constraint.Guideline
+ android:id="@+id/lib_g_m_v"
+ android:layout_width="1dp"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ app:layout_constraintGuide_percent="0.5" />
+
+ <TextView
+ android:id="@+id/lib_item_name"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:ellipsize="end"
+ android:gravity="start"
+ android:maxLines="1"
+ android:textSize="@dimen/textSizeLarge_openSource"
+ android:textStyle="normal"
+ app:layout_constraintEnd_toStartOf="@id/lib_g_m_v"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Library name" />
+
+ <TextView
+ android:id="@+id/lib_item_author"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="2dp"
+ android:gravity="end"
+ android:maxLines="2"
+ android:textSize="@dimen/textSizeSmall_openSource"
+ android:textStyle="normal"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/lib_g_m_v"
+ app:layout_constraintTop_toTopOf="parent"
+ tools:text="Author" />
+
+ <android.support.constraint.Barrier
+ android:id="@+id/lib_item_top_divider"
+ android:layout_width="wrap_content"
+ android:layout_height="1px"
+ android:layout_marginTop="@dimen/kau_spacing_micro"
+ app:barrierDirection="bottom"
+ app:constraint_referenced_ids="lib_item_name,lib_item_author"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <TextView
+ android:id="@+id/lib_item_description"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:maxLines="20"
+ android:paddingBottom="@dimen/kau_spacing_normal"
+ android:paddingTop="@dimen/kau_spacing_normal"
+ android:textSize="@dimen/textSizeSmall_openSource"
+ android:textStyle="normal"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/lib_item_top_divider"
+ tools:text="Description" />
+
+ <View
+ android:id="@+id/lib_item_bottom_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1px"
+ android:layout_marginTop="@dimen/kau_spacing_micro"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/lib_item_description" />
+
+ <TextView
+ android:id="@+id/lib_item_version"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginEnd="@dimen/kau_spacing_normal"
+ android:layout_marginTop="@dimen/kau_spacing_micro"
+ android:layout_weight="1"
+ android:gravity="start"
+ android:maxLines="1"
+ android:textSize="@dimen/textSizeSmall_openSource"
+ android:textStyle="normal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/lib_g_m_v"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/lib_item_bottom_divider"
+ tools:text="Version" />
+
+ <TextView
+ android:id="@+id/lib_item_license"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/kau_spacing_normal"
+ android:layout_marginTop="@dimen/kau_spacing_micro"
+ android:layout_weight="1"
+ android:gravity="end"
+ android:maxLines="1"
+ android:textSize="@dimen/textSizeSmall_openSource"
+ android:textStyle="normal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/lib_g_m_v"
+ app:layout_constraintTop_toBottomOf="@id/lib_item_bottom_divider"
+ tools:text="License" />
+
+ </android.support.constraint.ConstraintLayout>
+
+</android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_preference.xml b/core/src/main/res/layout/kau_preference.xml
new file mode 100644
index 0000000..c49951b
--- /dev/null
+++ b/core/src/main/res/layout/kau_preference.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--due to animations, we need a wrapper viewgroup so our changes will stick-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
+ android:clipToPadding="false"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:orientation="horizontal">
+
+ <android.support.constraint.ConstraintLayout
+ android:id="@+id/kau_pref_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/selectableItemBackground"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart">
+
+ <!--As per Android N, icons (24dp) are aligned to the left rather than centered-->
+
+ <ImageView
+ android:id="@+id/kau_pref_icon"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:layout_marginBottom="4dp"
+ android:layout_marginTop="4dp"
+ android:contentDescription="@string/kau_pref_icon"
+ android:paddingEnd="32dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.5"
+ tools:layout_editor_absoluteX="0dp" />
+
+ <TextView
+ android:id="@+id/kau_pref_title"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:ellipsize="marquee"
+ android:textAppearance="?android:attr/textAppearanceListItem"
+ android:textColor="?android:attr/textColorPrimary"
+ app:layout_constraintBottom_toTopOf="@+id/kau_pref_desc"
+ app:layout_constraintEnd_toStartOf="@+id/kau_pref_inner_frame"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintStart_toEndOf="@id/kau_pref_icon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_goneMarginBottom="16dp"
+ tools:layout_editor_absoluteX="-175dp" />
+
+ <TextView
+ android:id="@id/kau_pref_desc"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="16dp"
+ android:ellipsize="end"
+ android:maxLines="10"
+ android:textAppearance="?android:attr/textAppearanceListItemSecondary"
+ android:textColor="?android:attr/textColorSecondary"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/kau_pref_inner_frame"
+ app:layout_constraintHorizontal_bias="0"
+ app:layout_constraintStart_toEndOf="@id/kau_pref_icon"
+ app:layout_constraintTop_toBottomOf="@id/kau_pref_title"
+ tools:layout_editor_absoluteX="-175dp" />
+
+ <android.support.constraint.Barrier
+ android:id="@+id/kau_pref_barrier"
+ android:layout_width="1dp"
+ android:layout_height="wrap_content"
+ app:barrierDirection="end"
+ app:constraint_referenced_ids="kau_pref_title,kau_pref_desc"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <LinearLayout
+ android:id="@id/kau_pref_inner_frame"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:gravity="center_vertical|end"
+ android:orientation="horizontal"
+ android:paddingStart="16dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintHorizontal_bias="1.0"
+ app:layout_constraintStart_toEndOf="@id/kau_pref_barrier"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.5"
+ tools:layout_editor_absoluteX="1dp" />
+
+ </android.support.constraint.ConstraintLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_preference_checkbox.xml b/core/src/main/res/layout/kau_preference_checkbox.xml
new file mode 100644
index 0000000..5ab368b
--- /dev/null
+++ b/core/src/main/res/layout/kau_preference_checkbox.xml
@@ -0,0 +1,7 @@
+<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_pref_inner_content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:clickable="false"
+ android:background="@null" /> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_preference_color_preview.xml b/core/src/main/res/layout/kau_preference_color_preview.xml
new file mode 100644
index 0000000..2374971
--- /dev/null
+++ b/core/src/main/res/layout/kau_preference_color_preview.xml
@@ -0,0 +1,6 @@
+<ca.allanwang.kau.dialogs.color.CircleView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_pref_inner_content"
+ android:layout_width="40dp"
+ android:layout_height="40dp"
+ android:focusable="false"
+ android:clickable="false" /> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_preference_header.xml b/core/src/main/res/layout/kau_preference_header.xml
new file mode 100644
index 0000000..5deece3
--- /dev/null
+++ b/core/src/main/res/layout/kau_preference_header.xml
@@ -0,0 +1,10 @@
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_pref_title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="8dip"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingRight"
+ android:paddingStart="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingTop="16dip"
+ android:textColor="?android:attr/colorAccent"
+ android:textSize="14sp" /> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_preference_text.xml b/core/src/main/res/layout/kau_preference_text.xml
new file mode 100644
index 0000000..bbabdfb
--- /dev/null
+++ b/core/src/main/res/layout/kau_preference_text.xml
@@ -0,0 +1,7 @@
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/kau_pref_inner_content"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:focusable="false"
+ android:clickable="false"
+ android:background="@null" /> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_recycler_detached_background.xml b/core/src/main/res/layout/kau_recycler_detached_background.xml
new file mode 100644
index 0000000..7295d66
--- /dev/null
+++ b/core/src/main/res/layout/kau_recycler_detached_background.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout 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="wrap_content">
+
+ <!-- we use a parallel view for the background rather than just setting a background on the
+ recycler view for a nicer return transition. i.e. we want the background to fade and the
+ list to slide out separately -->
+ <View
+ android:id="@+id/kau_recycler_detached_background"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:colorBackground" />
+
+ <!--defaults to a LinearLayoutManager-->
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/kau_recycler_detached"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:clipToPadding="false"
+ android:paddingBottom="@dimen/kau_spacing_normal"
+ android:scrollbars="vertical"
+ app:layoutManager="android.support.v7.widget.LinearLayoutManager" />
+
+</FrameLayout>
diff --git a/core/src/main/res/layout/kau_recycler_textslider.xml b/core/src/main/res/layout/kau_recycler_textslider.xml
new file mode 100644
index 0000000..1402cfb
--- /dev/null
+++ b/core/src/main/res/layout/kau_recycler_textslider.xml
@@ -0,0 +1,34 @@
+<?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_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <android.support.v7.widget.Toolbar
+ android:id="@+id/kau_toolbar"
+ android:layout_width="0dp"
+ android:layout_height="?attr/actionBarSize"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <ca.allanwang.kau.widgets.TextSlider
+ android:id="@+id/kau_toolbar_text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ app:animation_type="slide_vertical" />
+
+ </android.support.v7.widget.Toolbar>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/kau_recycler"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/kau_toolbar" />
+
+</android.support.constraint.ConstraintLayout>
diff --git a/core/src/main/res/layout/kau_search_iitem.xml b/core/src/main/res/layout/kau_search_iitem.xml
new file mode 100644
index 0000000..efd7260
--- /dev/null
+++ b/core/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/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/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/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/search_desc"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toEndOf="@id/search_icon"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintVertical_bias="0.5" />
+
+ <TextView
+ android:id="@id/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/search_icon"
+ app:layout_constraintTop_toBottomOf="@id/search_title" />
+
+</android.support.constraint.ConstraintLayout> \ No newline at end of file
diff --git a/core/src/main/res/layout/kau_search_view.xml b/core/src/main/res/layout/kau_search_view.xml
new file mode 100644
index 0000000..ff9fd74
--- /dev/null
+++ b/core/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/search_shadow"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="@color/kau_shadow_overlay"
+ android:visibility="gone" />
+
+ <ca.allanwang.kau.views.BoundedCardView
+ 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: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/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/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/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/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/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/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/search_recycler"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+
+ android:overScrollMode="never"
+ android:visibility="gone" />
+
+ </LinearLayout>
+
+ </ca.allanwang.kau.views.BoundedCardView>
+
+</merge> \ No newline at end of file
diff --git a/core/src/main/res/transition/kau_about_return_downward.xml b/core/src/main/res/transition/kau_about_return_downward.xml
new file mode 100644
index 0000000..b040b1b
--- /dev/null
+++ b/core/src/main/res/transition/kau_about_return_downward.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2015 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<transitionSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together"
+ android:interpolator="@android:interpolator/fast_out_linear_in">
+
+ <slide
+ android:slideEdge="bottom"
+ android:duration="400">
+ <targets>
+ <target android:excludeId="@android:id/navigationBarBackground" />
+ <target android:excludeId="@android:id/statusBarBackground" />
+ </targets>
+ </slide>
+
+ <fade
+ android:startDelay="200"
+ android:duration="200">
+ <targets>
+ <target android:targetId="@id/about_indicator" />
+ <!--<target android:targetId="@id/libraries_intro" />-->
+ <!--<target android:targetId="@id/libs_list_background" />-->
+ </targets>
+ </fade>
+
+ <fade android:duration="400">
+ <targets>
+ <target android:targetId="@android:id/navigationBarBackground" />
+ <target android:targetId="@android:id/statusBarBackground" />
+ </targets>
+ </fade>
+
+</transitionSet>
diff --git a/core/src/main/res/transition/kau_about_return_upwards.xml b/core/src/main/res/transition/kau_about_return_upwards.xml
new file mode 100644
index 0000000..64b3f5e
--- /dev/null
+++ b/core/src/main/res/transition/kau_about_return_upwards.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2015 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<transitionSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together"
+ android:interpolator="@android:interpolator/fast_out_linear_in">
+
+ <slide
+ android:slideEdge="top"
+ android:duration="400">
+ <targets>
+ <target android:excludeId="@android:id/navigationBarBackground" />
+ <target android:excludeId="@android:id/statusBarBackground" />
+ </targets>
+ </slide>
+
+ <fade
+ android:startDelay="200"
+ android:duration="200">
+ <targets>
+ <target android:targetId="@id/about_indicator" />
+ <!--<target android:targetId="@id/libraries_intro" />-->
+ <!--<target android:targetId="@id/libs_list_background" />-->
+
+ </targets>
+ </fade>
+
+ <fade android:duration="400">
+ <targets>
+ <target android:targetId="@android:id/navigationBarBackground" />
+ <target android:targetId="@android:id/statusBarBackground" />
+ </targets>
+ </fade>
+
+</transitionSet>
diff --git a/core/src/main/res/transition/kau_enter_slide_top.xml b/core/src/main/res/transition/kau_enter_slide_top.xml
new file mode 100644
index 0000000..0089b84
--- /dev/null
+++ b/core/src/main/res/transition/kau_enter_slide_top.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2015 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<transitionSet
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:transitionOrdering="together"
+ android:duration="400"
+ android:interpolator="@android:interpolator/linear_out_slow_in">
+
+ <slide android:slideEdge="top">
+ <targets>
+ <target android:excludeId="@android:id/navigationBarBackground" />
+ <target android:excludeId="@android:id/statusBarBackground" />
+ </targets>
+ </slide>
+
+ <fade android:duration="@android:integer/config_mediumAnimTime">
+ <targets>
+ <target android:targetId="@android:id/navigationBarBackground" />
+ <target android:targetId="@android:id/statusBarBackground" />
+ </targets>
+ </fade>
+
+</transitionSet>
diff --git a/core/src/main/res/values/attr.xml b/core/src/main/res/values/attr.xml
new file mode 100644
index 0000000..49be8d9
--- /dev/null
+++ b/core/src/main/res/values/attr.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="ResourceName">
+
+ <declare-styleable name="BoundedCardView">
+ <attr name="maxHeight" format="dimension" />
+ <attr name="maxHeightPercent" format="float" />
+ </declare-styleable>
+
+ <declare-styleable name="TextSlider">
+ <attr name="animation_type" format="enum">
+ <enum name="none" value="1000" />
+ <enum name="slide_horizontal" value="1001" />
+ <enum name="slide_vertical" value="1002" />
+ </attr>
+ </declare-styleable>
+
+ <declare-styleable name="InkPageIndicator">
+ <attr name="dotDiameter" format="dimension" />
+ <attr name="dotGap" format="dimension" />
+ <attr name="animationDuration" format="integer" />
+ <attr name="pageIndicatorColor" format="color" />
+ <attr name="currentPageIndicatorColor" format="color" />
+ </declare-styleable>
+
+ <declare-styleable name="ElasticDragDismissFrameLayout">
+ <attr name="dragDismissDistance" format="dimension" />
+ <attr name="dragDismissFraction" format="float" />
+ <attr name="dragDismissScale" format="float" />
+ <attr name="dragElasticity" format="float" />
+ </declare-styleable>
+
+ <declare-styleable name="CutoutView">
+ <attr name="foregroundColor" format="color" />
+ <attr name="android:text" />
+ <attr name="android:drawable" />
+ <attr name="android:minHeight" />
+ <attr name="heightPercentageToScreen" format="float" />
+ <attr name="font"/>
+ </declare-styleable>
+
+</resources> \ No newline at end of file
diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml
new file mode 100644
index 0000000..6d51597
--- /dev/null
+++ b/core/src/main/res/values/colors.xml
@@ -0,0 +1,6 @@
+<resources>
+ <color name="kau_shadow_overlay">#80000000</color>
+
+ <color name="kau_about_page_indicator_dark">#80ffffff</color>
+ <color name="kau_about_page_indicator_dark_selected">#fff</color>
+</resources>
diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..5bf6da4
--- /dev/null
+++ b/core/src/main/res/values/dimens.xml
@@ -0,0 +1,37 @@
+<resources>
+ <!-- Default screen margins, per the Android Design guidelines. -->
+ <dimen name="kau_activity_horizontal_margin">16dp</dimen>
+ <dimen name="kau_activity_vertical_margin">16dp</dimen>
+ <dimen name="kau_dialog_margin">24dp</dimen>
+ <dimen name="kau_dialog_margin_bottom">16dp</dimen>
+
+ <dimen name="kau_fab_margin">16dp</dimen>
+ <dimen name="kau_appbar_padding_top">8dp</dimen>
+ <dimen name="kau_splash_logo">16dp</dimen>
+ <dimen name="kau_progress_bar_height">1dip</dimen>
+ <dimen name="kau_account_image_size">100dp</dimen>
+ <dimen name="kau_color_circle_size">56dp</dimen>
+
+ <dimen name="kau_status_bar_height">24dp</dimen>
+ <dimen name="kau_drag_dismiss_distance">112dp</dimen> <!-- 2 * ?android:actionBarSize -->
+
+ <dimen name="kau_spacing_normal">8dp</dimen>
+ <dimen name="kau_spacing_micro">4dp</dimen>
+ <dimen name="kau_spacing_large">32dp</dimen>
+ <dimen name="kau_spacing_xlarge">48dp</dimen>
+ <dimen name="kau_spacing_huge">64dp</dimen>
+ <dimen name="kau_padding_normal">16dp</dimen>
+ <dimen name="kau_padding_large">24dp</dimen>
+ <dimen name="kau_fab_size">56dp</dimen>
+ <dimen name="kau_fab_radius">28dp</dimen>
+ <dimen name="kau_display_4_text_size">112sp</dimen>
+
+ <dimen name="kau_about_header_height">224dp</dimen>
+
+ <!-- avatar should be a 40dp asset in a 48dp touch target & optically aligned with standard padding -->
+ <dimen name="kau_avatar_size">40dp</dimen>
+ <dimen name="kau_avatar_bounds">48dp</dimen>
+ <dimen name="kau_avatar_padding">4dp</dimen> <!-- (avatar_bounds - avatar_size) / 2 -->
+ <dimen name="kau_avatar_margin">12dp</dimen> <!-- padding_normal - avatar_padding -->
+ <dimen name="kau_avatar_ripple_radius">20dp</dimen> <!-- avatar_size / 2 -->
+</resources>
diff --git a/core/src/main/res/values/dimens_search.xml b/core/src/main/res/values/dimens_search.xml
new file mode 100644
index 0000000..acf79cb
--- /dev/null
+++ b/core/src/main/res/values/dimens_search.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/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml
new file mode 100644
index 0000000..dd42392
--- /dev/null
+++ b/core/src/main/res/values/ids.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <item name="kau_item_account" type="id" />
+ <item name="kau_item_pref_header" type="id" />
+ <item name="kau_item_pref_text" type="id" />
+ <item name="kau_item_pref_checkbox" type="id" />
+ <item name="kau_item_pref_color_picker" type="id" />
+ <item name="kau_item_pref_sub_item" type="id" />
+ <item name="kau_item_pref_plain_text" type="id" />
+ <item name="kau_item_search" type="id" />
+ <item name="kau_item_cutout" type="id" />
+ <item name="kau_item_header_big_margin_top" type="id" />
+ <item name="kau_item_card" type="id" />
+ <item name="kau_item_library" type="id" />
+ <item name="kau_item_about_main" type="id" />
+</resources> \ No newline at end of file
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
new file mode 100644
index 0000000..5ea5a23
--- /dev/null
+++ b/core/src/main/res/values/strings.xml
@@ -0,0 +1,15 @@
+<resources>
+ <string name="kau_u2022">•</string>
+ <string name="kau_color_picker">Color Picker</string>
+
+ <!--Color Picker-->
+ <string name="kau_md_custom">Custom</string>
+ <string name="kau_md_presets">Presets</string>
+ <string name="kau_md_color_palette">Color Palette</string>
+
+ <string name="kau_kpref_title_placeholder">Title Placeholder</string>
+ <string name="kau_pref_icon">Pref Icon</string>
+
+ <string name="kau_about_libraries_intro">This app would not be possible without the following great libraries.</string>
+ <string name="kau_dependencies_used">Dependencies Used</string>
+</resources>
diff --git a/core/src/main/res/values/strings_about.xml b/core/src/main/res/values/strings_about.xml
new file mode 100644
index 0000000..e659fc3
--- /dev/null
+++ b/core/src/main/res/values/strings_about.xml
@@ -0,0 +1,27 @@
+<resources xmlns:tools="http://schemas.android.com/tools">
+ <string name="library_kau_libraryVersion" tools:ignore="ResourceName">1.4</string>
+
+ <string name="define_kau" tools:ignore="ResourceName" />
+ <!-- Author section -->
+ <string name="library_kau_author" tools:ignore="ResourceName">Allan Wang</string>
+ <string name="library_kau_authorWebsite" tools:ignore="ResourceName">https://www.allanwang.ca/dev/</string>
+ <!-- Library section -->
+ <string name="library_kau_libraryName" tools:ignore="ResourceName">KAU</string>
+ <string name="library_kau_libraryDescription" tools:ignore="ResourceName">
+ <![CDATA[
+ An extensive collection of Kotlin Android Utilities.
+ <br/><br/>
+ KAU aims to make many common functions executable in one line. It adds numerous extensions to match Kotlin\'s DSL,
+ and supports completely customizable view groups that are used in almost any app project.
+ ]]>
+ </string>
+ <string name="library_kau_libraryWebsite" tools:ignore="ResourceName">https://allanwang.github.io/KAU/</string>
+ <!-- OpenSource section -->
+ <string name="library_kau_isOpenSource" tools:ignore="ResourceName">true</string>
+ <string name="library_kau_repositoryLink" tools:ignore="ResourceName">https://github.com/AllanWang/KAU</string>
+ <!-- ClassPath for autoDetect section -->
+ <string name="library_kau_classPath" tools:ignore="ResourceName">ca.allanwang.kau</string>
+ <!-- License section -->
+ <string name="library_kau_licenseId" tools:ignore="ResourceName">apache_2_0</string>
+ <!-- Custom variables section -->
+</resources> \ No newline at end of file
diff --git a/core/src/main/res/values/strings_commons.xml b/core/src/main/res/values/strings_commons.xml
new file mode 100644
index 0000000..0ca785d
--- /dev/null
+++ b/core/src/main/res/values/strings_commons.xml
@@ -0,0 +1,57 @@
+<!--
+A collection of common string values
+Most resources are verbatim and x represents a formatted item
+-->
+
+<resources>
+ <string name="kau_add_account">Add Account</string>
+ <string name="kau_amoled" translatable="false">AMOLED</string>
+ <string name="kau_back">Back</string>
+ <string name="kau_cancel">Cancel</string>
+ <string name="kau_changelog">Changelog</string>
+ <string name="kau_close">Close</string>
+ <string name="kau_contact_us">Contact Us</string>
+ <string name="kau_custom">Custom</string>
+ <string name="kau_dark">Dark</string>
+ <string name="kau_default">Default</string>
+ <string name="kau_do_not_show_again">Do not show again</string>
+ <string name="kau_done">Done</string>
+ <string name="kau_error">Error</string>
+ <string name="kau_exit">Exit</string>
+ <string name="kau_exit_confirmation">Are you sure you want to exit?</string>
+ <string name="kau_exit_confirmation_x">Are you sure you want to exit %s?</string>
+ <string name="kau_glass">Glass</string>
+ <string name="kau_got_it">Got it</string>
+ <string name="kau_great">Great</string>
+ <string name="kau_hide">Hide</string>
+ <string name="kau_light">Light</string>
+ <string name="kau_login">Login</string>
+ <string name="kau_logout">Logout</string>
+ <string name="kau_logout_confirm_as_x">Are you sure you want to log out as %s?</string>
+ <string name="kau_lorem_ipsum" translatable="false">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</string>
+ <string name="kau_manage_account">Manage Account</string>
+ <string name="kau_maybe">Maybe</string>
+ <string name="kau_menu">Menu</string>
+ <string name="kau_no">No</string>
+ <string name="kau_none">None</string>
+ <string name="kau_ok">@android:string/ok</string>
+ <string name="kau_one_day">1 day</string>
+ <string name="kau_one_hour">1 hour</string>
+ <string name="kau_play_store">Play Store</string>
+ <string name="kau_rate">Rate</string>
+ <string name="kau_report_bug">Report A Bug</string>
+ <string name="kau_send_feedback">Send Feedback</string>
+ <string name="kau_settings">Settings</string>
+ <string name="kau_thank_you">Thank You</string>
+ <string name="kau_uh_oh">Uh Oh</string>
+ <string name="kau_x_days">%d days</string>
+ <string name="kau_x_hours">%d hours</string>
+ <string name="kau_x_minutes">%d minutes</string>
+ <string name="kau_warning">Warning</string>
+ <string name="kau_yes">Yes</string>
+ <string name="kau_search">Search</string>
+ <string name="kau_no_results_found">No Results Found</string>
+ <string name="kau_about_app">About App</string>
+ <string name="kau_send_via">Send via</string>
+ <string name="kau_about_x">About %s</string>
+</resources>
diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml
new file mode 100644
index 0000000..2067c6d
--- /dev/null
+++ b/core/src/main/res/values/styles.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <style name="Kau" parent="Theme.AppCompat.NoActionBar" />
+
+ <style name="Kau.Translucent">
+ <item name="android:windowBackground">@color/kau_shadow_overlay</item>
+ <item name="android:colorBackgroundCacheHint">@null</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowIsFloating">false</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowDrawsSystemBarBackgrounds">true</item>
+ </style>
+
+ <style name="Kau.Translucent.About">
+ <item name="android:windowEnterTransition">@transition/kau_enter_slide_top</item>
+ <item name="android:windowReturnTransition">@transition/kau_about_return_upwards</item>
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/core/src/main/res/values/styles_animations.xml b/core/src/main/res/values/styles_animations.xml
new file mode 100644
index 0000000..a991132
--- /dev/null
+++ b/core/src/main/res/values/styles_animations.xml
@@ -0,0 +1,30 @@
+<resources>
+
+ <style name="KauSlideIn" parent="@android:style/Animation.Activity">
+ <item name="android:activityOpenEnterAnimation">@anim/kau_slide_in_right</item>
+ <item name="android:activityCloseEnterAnimation">@anim/kau_slide_in_right</item>
+ <item name="android:taskOpenEnterAnimation">@anim/kau_slide_in_right</item>
+ <item name="android:taskCloseEnterAnimation">@anim/kau_slide_in_right</item>
+ <item name="android:taskToFrontEnterAnimation">@anim/kau_slide_in_right</item>
+ <item name="android:windowEnterAnimation">@anim/kau_slide_in_right</item>
+ </style>
+
+ <style name="KauSlideInSlideOut" parent="@style/KauSlideIn">
+ <item name="android:activityOpenExitAnimation">@anim/kau_slide_out_right</item>
+ <item name="android:activityCloseExitAnimation">@anim/kau_slide_out_right</item>
+ <item name="android:taskOpenExitAnimation">@anim/kau_slide_out_right</item>
+ <item name="android:taskCloseExitAnimation">@anim/kau_slide_out_right</item>
+ <item name="android:taskToFrontExitAnimation">@anim/kau_slide_out_right</item>
+ <item name="android:windowExitAnimation">@anim/kau_slide_out_right</item>
+ </style>
+
+ <style name="KauSlideInFadeOut" parent="@style/KauSlideIn">
+ <item name="android:activityOpenExitAnimation">@anim/kau_fade_out</item>
+ <item name="android:activityCloseExitAnimation">@anim/kau_fade_out</item>
+ <item name="android:taskOpenExitAnimation">@anim/kau_fade_out</item>
+ <item name="android:taskCloseExitAnimation">@anim/kau_fade_out</item>
+ <item name="android:taskToFrontExitAnimation">@anim/kau_fade_out</item>
+ <item name="android:windowExitAnimation">@anim/kau_fade_out</item>
+ </style>
+
+</resources>
diff --git a/core/src/main/res/xml/kau_changelog.xml b/core/src/main/res/xml/kau_changelog.xml
new file mode 100644
index 0000000..e570995
--- /dev/null
+++ b/core/src/main/res/xml/kau_changelog.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <!--This is a template-->
+
+ <!--
+ <version title="v"/>
+ <item text="" />
+ -->
+
+ <version title="v0.1" />
+ <item text="Initial Changelog" />
+ <item text="" />
+</resources> \ No newline at end of file
diff --git a/core/src/test/kotlin/ca/allanwang/kprefs/library/UtilsTest.kt b/core/src/test/kotlin/ca/allanwang/kprefs/library/UtilsTest.kt
new file mode 100644
index 0000000..c2c96e4
--- /dev/null
+++ b/core/src/test/kotlin/ca/allanwang/kprefs/library/UtilsTest.kt
@@ -0,0 +1,25 @@
+package ca.allanwang.kprefs.library
+
+import android.graphics.Color
+import ca.allanwang.kau.utils.round
+import ca.allanwang.kau.utils.toHexString
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * Created by Allan Wang on 2017-06-23.
+ */
+class UtilsTest {
+
+ @Test
+ fun colorToHex() {
+ assertEquals("#ffffff", Color.WHITE.toHexString(withAlpha = false, withHexPrefix = true).toLowerCase())
+ }
+
+ @Test
+ fun rounding() {
+ assertEquals("1.23", 1.23456f.round(2))
+ assertEquals("22.466", 22.465920439.round(3))
+ assertEquals("22", 22f.round(3))
+ }
+} \ No newline at end of file