aboutsummaryrefslogtreecommitdiff
path: root/library/src/main/kotlin/ca/allanwang
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-06-28 17:08:20 -0700
committerAllan Wang <me@allanwang.ca>2017-06-28 17:08:20 -0700
commitd52c787508bf5ba0023946228468731a82a1132d (patch)
treea5048581dc74c9f0a87d43d83eb7e7d2c69ff309 /library/src/main/kotlin/ca/allanwang
parent0d7bf678f84f60a11e7c7cdbaa31bfe6ef21166c (diff)
downloadkau-d52c787508bf5ba0023946228468731a82a1132d.tar.gz
kau-d52c787508bf5ba0023946228468731a82a1132d.tar.bz2
kau-d52c787508bf5ba0023946228468731a82a1132d.zip
Beginning of overlaying about activity
Diffstat (limited to 'library/src/main/kotlin/ca/allanwang')
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt181
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt18
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt68
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt50
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt2
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt2
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt16
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt8
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt32
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt29
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt23
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt8
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/views/KauCutoutTextView.kt147
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/widgets/KauElasticDragDismissFrameLayout.kt245
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/widgets/KauInkPageIndicator.java850
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/widgets/KauTextSlider.kt (renamed from library/src/main/kotlin/ca/allanwang/kau/views/KauTextSlider.kt)2
16 files changed, 1531 insertions, 150 deletions
diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt b/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
index 4690718..476b7f8 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/about/AboutActivityBase.kt
@@ -1,84 +1,157 @@
package ca.allanwang.kau.about
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.LinearLayoutManager
import android.support.v7.widget.RecyclerView
-import android.support.v7.widget.Toolbar
+import android.transition.TransitionInflater
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.TextView
import ca.allanwang.kau.R
-import ca.allanwang.kau.adapters.ChainedAdapters
-import ca.allanwang.kau.adapters.SectionAdapter
-import ca.allanwang.kau.animators.SlideUpAlphaAnimator
+import ca.allanwang.kau.logging.KL
import ca.allanwang.kau.utils.bindView
+import ca.allanwang.kau.utils.dimenPixelSize
import ca.allanwang.kau.utils.string
-import ca.allanwang.kau.views.KauTextSlider
+import ca.allanwang.kau.views.KauCutoutTextView
+import ca.allanwang.kau.widgets.KauElasticDragDismissFrameLayout
+import ca.allanwang.kau.widgets.KauInkPageIndicator
import com.mikepenz.aboutlibraries.Libs
import com.mikepenz.aboutlibraries.entity.Library
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
+import java.security.InvalidParameterException
/**
- * Created by Allan Wang on 2017-06-26.
- *
- * Customizable About Activity
- * Will automatically include a section containing all of the libraries registered under About Libraries
- * Takes in [rClass], which is the R.string::class.java for your app
- * It is used to get the libs dynamically
- * Make sure to add the following if you are using proguard:
- * # About library
- * -keep class .R
- * -keep class **.R$* {
- * <fields>;
- * }
+ * 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
*/
-open class AboutActivityBase(val rClass: Class<*>) : AppCompatActivity() {
+abstract class AboutActivityBase(val rClass: Class<*>, val configBuilder: Configs.() -> Unit = {}) : AppCompatActivity() {
- val toolbar: Toolbar by bindView(R.id.kau_toolbar)
- val toolbarText: KauTextSlider by bindView(R.id.kau_toolbar_text)
- val recycler: RecyclerView by bindView(R.id.kau_recycler)
- val libSection: Pair<String, SectionAdapter<LibraryItem>> by lazy { string(R.string.kau_dependencies_used) to SectionAdapter<LibraryItem>() }
- val sectionsChain: ChainedAdapters<String> = ChainedAdapters()
+ val draggableFrame: KauElasticDragDismissFrameLayout by bindView(R.id.about_draggable_frame)
+ val pager: ViewPager by bindView(R.id.about_pager)
+ val indicator: KauInkPageIndicator by bindView(R.id.about_indicator)
+ val configs: Configs by lazy { Configs().apply { configBuilder() } }
- fun addLibsAsync() {
- doAsync {
- val libs = Libs(this@AboutActivityBase, Libs.toStringArray(rClass.fields))
- val items = getLibraries(libs)
- uiThread { libSection.second.add(items.map { LibraryItem(it) }) }
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.kau_activity_about)
+ with(pager) {
+ adapter = AboutPagerAdapter()
+ pageMargin = dimenPixelSize(R.dimen.kau_spacing_normal)
}
+ indicator.setViewPager(pager)
+ draggableFrame.addListener(object : KauElasticDragDismissFrameLayout.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()
+ }
+ })
}
- /**
- * By default, the libraries will be extracted dynamically and sorted
- * Override this to define your own list
- */
- open fun getLibraries(libs: Libs): List<Library> = libs.prepareLibraries(this@AboutActivityBase, null, null, true, true)
+ inner class Configs {
+ var cutoutTextRes: Int = -1
+ val cutoutText: String? = null
+ var mainPageTitleRes: Int = -1
+ var mainPageTitle: String = "Kau test"
+ var libPageTitleRes: Int = -1
+ var libPageTitle: String? = string(R.string.kau_about_libraries_intro)
+ var transitionExitReversed: Int = R.transition.kau_about_return_downward
+ }
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.kau_activity_about)
- val sections = onCreateSections()
- sectionsChain.add(sections)
- if (!sections.contains(libSection)) sectionsChain.add(libSection)
- sectionsChain.bindRecyclerView(recycler) {
- item, _, dy ->
- if (dy > 0) toolbarText.setNextText(item)
- else toolbarText.setPrevText()
+ open fun getLibraries(libs: Libs): List<Library> = libs.prepareLibraries(this, null, null, true, true)
+
+ open val pageCount: Int = 2
+
+ open fun getPage(position: Int, layoutInflater: LayoutInflater, parent: ViewGroup): View {
+ KL.e("Get page $position")
+ return when (position) {
+ 0 -> inflateMainPage(layoutInflater, parent)
+ pageCount - 1 -> inflateLibPage(layoutInflater, parent)
+ else -> throw InvalidParameterException()
}
- recycler.itemAnimator = SlideUpAlphaAnimator()
- toolbarText.setCurrentText(sectionsChain[0].first)
- onPostCreate()
- addLibsAsync()
}
+ fun inflateMainPage(layoutInflater: LayoutInflater, parent: ViewGroup): View {
+ val v = layoutInflater.inflate(R.layout.kau_about_section_main, parent, false)
+ postInflateMainPage(
+ v.findViewById<KauCutoutTextView>(R.id.about_main_cutout),
+ v.findViewById<FrameLayout>(R.id.about_main_bottom_container),
+ v.findViewById<TextView>(R.id.about_main_bottom_text)
+ )
+ return v
+ }
- open fun onPostCreate() {
+ open fun postInflateMainPage(cutout: KauCutoutTextView, bottomContainer: FrameLayout, bottomText: TextView) {
+ with (configs) {
+ cutout.text = string(cutoutTextRes, cutoutText)
+ bottomText.text = string(mainPageTitleRes, mainPageTitle)
+ }
+ }
+ fun inflateLibPage(layoutInflater: LayoutInflater, parent: ViewGroup): View {
+ val v = layoutInflater.inflate(R.layout.kau_about_section_libraries, parent, false)
+ postInflateLibPage(
+ v.findViewById<TextView>(R.id.about_library_title),
+ v.findViewById<RecyclerView>(R.id.about_library_recycler)
+ )
+ return v
}
- /**
- * Get all the header adapters
- * The adapters should be listed in the order that they appear,
- * and if the [libSection] shouldn't be at the end, it should be added in this list
- */
- open fun onCreateSections(): List<Pair<String, SectionAdapter<*>>> = listOf(libSection)
+ open fun postInflateLibPage(title: TextView, recycler: RecyclerView) {
+ title.text = string(configs.libPageTitleRes, configs.libPageTitle)
+ val libAdapter = FastItemAdapter<LibraryItem>()
+ with(recycler) {
+ layoutManager = LinearLayoutManager(this@AboutActivityBase)
+ adapter = libAdapter
+ }
+ doAsync {
+ val libs = getLibraries(Libs(this@AboutActivityBase, Libs.toStringArray(rClass.fields))).map { LibraryItem(it) }
+ uiThread { libAdapter.add(libs) }
+ }
+ }
+
+ 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)
+ KL.e("Add view")
+ 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`
+
+ private fun getPage(position: Int, parent: ViewGroup): View {
+ KL.e("Create page $position ${views[position] == null}")
+ if (views[position] == null) views[position] = getPage(position, layoutInflater, parent)
+ return views[position]!!
+ }
+ }
} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt b/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt
index 0dd4973..c3bfb5a 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/about/LibraryItem.kt
@@ -66,16 +66,16 @@ class LibraryItem(val lib: Library) : AbstractItem<LibraryItem, LibraryItem.View
override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
- val card: CardView by bindView(R.id.rippleForegroundListenerView)
- val name: TextView by bindView(R.id.libraryName)
- val creator: TextView by bindView(R.id.libraryCreator)
- val description: TextView by bindView(R.id.libraryDescription)
- val version: TextView by bindView(R.id.libraryVersion)
- val license: TextView by bindView(R.id.libraryLicense)
- val bottomContainer: LinearLayout by bindView(R.id.libraryBottomContainer)
+ 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 bottomContainer: LinearLayout by bindView(R.id.lib_item_bottom_container)
- val divider: View by bindView(R.id.libraryDescriptionDivider)
- val bottomDivider: View by bindView(R.id.libraryBottomDivider)
+ 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/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt b/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt
deleted file mode 100644
index 3edad32..0000000
--- a/library/src/main/kotlin/ca/allanwang/kau/about/MainItem.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-package ca.allanwang.kau.about
-
-import android.graphics.drawable.Drawable
-import android.support.v7.widget.CardView
-import android.support.v7.widget.RecyclerView
-import android.view.View
-import android.widget.LinearLayout
-import android.widget.TextView
-import ca.allanwang.kau.R
-import ca.allanwang.kau.utils.bindView
-import com.mikepenz.fastadapter.items.AbstractItem
-
-/**
- * Created by Allan Wang on 2017-06-27.
- */
-class MainItem(builder: Config.() -> Unit) : AbstractItem<MainItem, MainItem.ViewHolder>() {
-
- val configs = Config().apply { builder() }
-
- class Config {
- var icon: Drawable? = null
- var title: String = "App Title"
- var author: String? = null
- var description: String? = null
- var version: String? = null
- var githubLink: String? = null
- }
-
- override fun getType(): Int = R.id.kau_item_about_main
-
- override fun getLayoutRes(): Int = R.layout.kau_about_item_main
-
- override fun isSelectable(): Boolean = false
-
- override fun getViewHolder(v: View): ViewHolder = ViewHolder(v)
-
- override fun bindView(holder: ViewHolder, payloads: MutableList<Any>?) {
- super.bindView(holder, payloads)
- with(holder) {
- title.text = configs.title
- creator.text = configs.author
- description.text = configs.description
-// license.text = configs.description
- }
- }
-
- override fun unbindView(holder: ViewHolder) {
- super.unbindView(holder)
- with(holder) {
- title.text = null
- creator.text = null
- description.text = null
- }
- }
-
- class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
- val card: CardView by bindView(R.id.container)
- val title: TextView by bindView(R.id.title)
- val creator: TextView by bindView(R.id.creator)
- val description: TextView by bindView(R.id.description)
- val version: TextView by bindView(R.id.version)
- val license: TextView by bindView(R.id.license)
- val bottomContainer: LinearLayout by bindView(R.id.bottom_container)
-
- val divider: View by bindView(R.id.top_divider)
- val bottomDivider: View by bindView(R.id.bottom_divider)
- }
-}
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt b/library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyContext.kt
new file mode 100644
index 0000000..a7dc09e
--- /dev/null
+++ b/library/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 get(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/library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt b/library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
index b74c5c7..f8947f3 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt
@@ -9,7 +9,7 @@ import kotlin.reflect.KProperty
* Lazy delegate that can be invalidated if needed
* https://stackoverflow.com/a/37294840/4407321
*/
-private object UNINITIALIZED
+internal object UNINITIALIZED
fun <T : Any> lazyResettable(initializer: () -> T): LazyResettable<T> = LazyResettable<T>(initializer)
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt
index 193ca58..a2985f0 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefActivity.kt
@@ -17,7 +17,7 @@ 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.views.KauTextSlider
+import ca.allanwang.kau.widgets.KauTextSlider
import ca.allanwang.kau.views.RippleCanvas
import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt
new file mode 100644
index 0000000..3739dec
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimHolder.kt
@@ -0,0 +1,16 @@
+package ca.allanwang.kau.utils
+
+import android.view.animation.AnimationUtils
+import ca.allanwang.kau.kotlin.lazyContext
+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)
+
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
index cde332a..86b049e 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
@@ -2,13 +2,16 @@ 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.
@@ -18,7 +21,7 @@ import android.widget.TextView
@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) {
+ oldRight: Int, oldBottom: Int) {
v.removeOnLayoutChangeListener(this)
var x2 = x
var y2 = y
@@ -147,5 +150,4 @@ import android.widget.TextView
})
}
-@KauUtils fun TextView.setTextWithFade(@StringRes textId: Int, duration: Long = 200, onFinish: (() -> Unit)? = null) = setTextWithFade(context.getString(textId), duration, 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/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
index 31bff97..876f634 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
@@ -1,6 +1,7 @@
package ca.allanwang.kau.utils
import android.app.Activity
+import android.app.ActivityOptions
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
@@ -12,20 +13,28 @@ import android.support.v4.app.ActivityOptionsCompat
import android.support.v4.content.ContextCompat
import android.util.TypedValue
import android.view.View
-import android.view.inputmethod.InputMethodManager
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, intentBuilder: Intent.() -> Unit = {}, bundle: Bundle? = null) {
+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()
- ContextCompat.startActivity(this, intent, bundle)
+ 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()
}
@@ -35,7 +44,7 @@ fun Context.startActivity(clazz: Class<out Activity>, clearStack: Boolean = fals
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, bundle)
+ startActivity(clazz, clearStack, intentBuilder = intentBuilder, bundle = bundle)
}
/**
@@ -47,7 +56,7 @@ fun Context.startActivitySlideIn(clazz: Class<out Activity>, clearStack: Boolean
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, bundle)
+ startActivity(clazz, clearStack, intentBuilder = intentBuilder, bundle = bundle)
}
fun Context.startPlayStoreLink(@StringRes packageIdRes: Int) = startPlayStoreLink(string(packageIdRes))
@@ -71,6 +80,7 @@ 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)
//Attr retrievers
@@ -127,3 +137,15 @@ fun Context.getDip(value: Float): Float = TypedValue.applyDimension(TypedValue.C
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.
+ */
+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
+ } \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/FontUtils.kt
new file mode 100644
index 0000000..3fc509d
--- /dev/null
+++ b/library/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/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
index 84d7e50..0f070d1 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
@@ -2,6 +2,8 @@ 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.design.widget.Snackbar
@@ -9,9 +11,11 @@ import android.support.transition.AutoTransition
import android.support.transition.TransitionManager
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
@@ -78,4 +82,21 @@ fun View.snackbar(@StringRes textId: Int, duration: Int = Snackbar.LENGTH_LONG,
}
@KauUtils val View.parentViewGroup: ViewGroup
- get() = parent as ViewGroup \ No newline at end of file
+ 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)
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt b/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt
index a07a118..60f5176 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/views/KauBoundedCardView.kt
@@ -6,6 +6,7 @@ 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
/**
@@ -19,13 +20,6 @@ class KauBoundedCardView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : CardView(context, attrs, defStyleAttr) {
- val parentVisibleHeight: Int
- get() {
- val r = Rect()
- parentViewGroup.getWindowVisibleDisplayFrame(r)
- return r.height()
- }
-
/**
* Maximum height possible, defined in dp (will be converted to px)
* Defaults to parent's visible height
diff --git a/library/src/main/kotlin/ca/allanwang/kau/views/KauCutoutTextView.kt b/library/src/main/kotlin/ca/allanwang/kau/views/KauCutoutTextView.kt
new file mode 100644
index 0000000..8df604a
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/views/KauCutoutTextView.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.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.logging.KL
+import ca.allanwang.kau.utils.dimenPixelSize
+import ca.allanwang.kau.utils.getFont
+import ca.allanwang.kau.utils.parentVisibleHeight
+
+/**
+ * A view which punches out some text from an opaque color block, allowing you to see through it.
+ */
+class KauCutoutTextView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+ private val textPaint: TextPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
+ private var cutout: Bitmap? = null
+ var foregroundColor = Color.MAGENTA
+ var text: String? = "Text"
+ var overlayType: Int = 0 //todo add vector overlay options
+ private var textSize: Float = 0f
+ private var textY: Float = 0f
+ private var textX: Float = 0f
+ 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.KauCutoutTextView, 0, 0)
+ if (a.hasValue(R.styleable.KauCutoutTextView_kau_font))
+ textPaint.typeface = context.getFont(a.getString(R.styleable.KauCutoutTextView_kau_font))
+ foregroundColor = a.getColor(R.styleable.KauCutoutTextView_kau_foregroundColor, foregroundColor)
+ text = a.getString(R.styleable.KauCutoutTextView_android_text) ?: text
+ minHeight = a.getDimension(R.styleable.KauCutoutTextView_android_minHeight, minHeight)
+ heightPercentage = a.getFloat(R.styleable.KauCutoutTextView_kau_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)
+ calculateTextPosition()
+ createBitmap()
+ KL.d("Size changed")
+ }
+
+ private fun calculateTextPosition() {
+ val targetWidth = width / PHI
+ textSize = getSingleLineTextSize(text!!, textPaint, targetWidth, 0f, maxTextSize,
+ 0.5f, resources.displayMetrics)
+ textPaint.textSize = textSize
+
+ // measuring text is fun :] see: https://chris.banes.me/2014/03/27/measuring-text/
+ textX = (width - textPaint.measureText(text)) / 2
+ val textBounds = Rect()
+ textPaint.getTextBounds(text, 0, text!!.length, textBounds)
+ val textHeight = textBounds.height().toFloat()
+ textY = (height + textHeight) / 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)
+
+ if (high - low < precision) {
+ return low
+ } else if (maxLineWidth > targetWidth) {
+ return getSingleLineTextSize(text, paint, targetWidth, low, mid, precision, metrics)
+ } else if (maxLineWidth < targetWidth) {
+ return getSingleLineTextSize(text, paint, targetWidth, mid, high, precision, metrics)
+ } else {
+ return 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)
+
+ // this is the magic – Clear mode punches out the bitmap
+ textPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR)
+ cutoutCanvas.drawText(text, textX, textY, textPaint)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.drawBitmap(cutout!!, 0f, 0f, null)
+ }
+
+ override fun hasOverlappingRendering(): Boolean = true
+
+ companion object {
+ val PHI = 1.6182f
+ }
+}
diff --git a/library/src/main/kotlin/ca/allanwang/kau/widgets/KauElasticDragDismissFrameLayout.kt b/library/src/main/kotlin/ca/allanwang/kau/widgets/KauElasticDragDismissFrameLayout.kt
new file mode 100644
index 0000000..d186647
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/widgets/KauElasticDragDismissFrameLayout.kt
@@ -0,0 +1,245 @@
+/*
+ * 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.logging.KL
+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 KauElasticDragDismissFrameLayout @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 {
+
+ val a = getContext().obtainStyledAttributes(
+ attrs, R.styleable.KauElasticDragDismissFrameLayout, 0, 0)
+
+ if (a.hasValue(R.styleable.KauElasticDragDismissFrameLayout_kau_dragDismissDistance)) {
+ dragDismissDistance = a.getDimensionPixelSize(R.styleable.KauElasticDragDismissFrameLayout_kau_dragDismissDistance, 0).toFloat()
+ } else if (a.hasValue(R.styleable.KauElasticDragDismissFrameLayout_kau_dragDismissFraction)) {
+ dragDismissFraction = a.getFloat(R.styleable.KauElasticDragDismissFrameLayout_kau_dragDismissFraction, dragDismissFraction)
+ }
+ if (a.hasValue(R.styleable.KauElasticDragDismissFrameLayout_kau_dragDismissScale)) {
+ dragDismissScale = a.getFloat(R.styleable.KauElasticDragDismissFrameLayout_kau_dragDismissScale, dragDismissScale)
+ shouldScale = dragDismissScale != 1f
+ }
+ if (a.hasValue(R.styleable.KauElasticDragDismissFrameLayout_kau_dragElasticity)) {
+ dragElacticity = a.getFloat(R.styleable.KauElasticDragDismissFrameLayout_kau_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)
+ KL.e("On $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/library/src/main/kotlin/ca/allanwang/kau/widgets/KauInkPageIndicator.java b/library/src/main/kotlin/ca/allanwang/kau/widgets/KauInkPageIndicator.java
new file mode 100644
index 0000000..1f2dc9a
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/widgets/KauInkPageIndicator.java
@@ -0,0 +1,850 @@
+/*
+ * 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.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;
+
+/**
+ * An ink inspired widget for indicating pages in a {@link ViewPager}.
+ */
+public class KauInkPageIndicator 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;
+
+ // 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 KauInkPageIndicator(Context context) {
+ this(context, null, 0);
+ }
+
+ public KauInkPageIndicator(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public KauInkPageIndicator(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.KauInkPageIndicator, defStyle, 0);
+
+ dotDiameter = a.getDimensionPixelSize(R.styleable.KauInkPageIndicator_kau_dotDiameter,
+ DEFAULT_DOT_SIZE * density);
+ dotRadius = dotDiameter / 2;
+ halfDotRadius = dotRadius / 2;
+ gap = a.getDimensionPixelSize(R.styleable.KauInkPageIndicator_kau_dotGap,
+ DEFAULT_GAP * density);
+ animDuration = (long) a.getInteger(R.styleable.KauInkPageIndicator_kau_animationDuration,
+ DEFAULT_ANIM_DURATION);
+ animHalfDuration = animDuration / 2;
+ unselectedColour = a.getColor(R.styleable.KauInkPageIndicator_kau_pageIndicatorColor,
+ DEFAULT_UNSELECTED_COLOUR);
+ selectedColour = a.getColor(R.styleable.KauInkPageIndicator_kau_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().get(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(KauInkPageIndicator.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/library/src/main/kotlin/ca/allanwang/kau/views/KauTextSlider.kt b/library/src/main/kotlin/ca/allanwang/kau/widgets/KauTextSlider.kt
index 8294f9f..02b4912 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/views/KauTextSlider.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/widgets/KauTextSlider.kt
@@ -1,4 +1,4 @@
-package ca.allanwang.kau.views
+package ca.allanwang.kau.widgets
import android.content.Context
import android.graphics.Color