From 48213d0b427c478865c75fee912ff1ae8bbaffb5 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Mon, 31 Jul 2017 23:02:01 -0700 Subject: Major update to core and kotterknife; create mediapicker (#15) * Readme * Fix kau direction bits * Truly support transparent ripples * Update changelog * Test rect as base * Replace fab transition with generic fade scale transition * Add scalexy func * Add scaleXY * Add arguments to fadeScaleTransition * Clean up ink indicator * Create setOnSingleTapListener * Fix lint and add rndColor * Create kotterknife resettables * Add readme and missing object * Create lazy resettable registered * Update core docs * Opt for separate class for resettable registry * Clean up resettable registry * Rename functions * Add ripple callback listener * Adjust kprefactivity desc color * Add more transitions * Add delete keys option * Add instrumentation tests * switch id * Revert automatic instrumental tests * Generify imagepickercore and prepare video alternative * Create working video picker * Address possible null issue * Update searchview * Make layouts public * Add changelog test * Update logo link * Add custom color gif --- .../kotlin/ca/allanwang/kau/changelog/Changelog.kt | 103 ------------ .../ca/allanwang/kau/kotlin/LazyResettable.kt | 30 +++- .../main/kotlin/ca/allanwang/kau/kpref/KPref.kt | 21 +++ .../kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt | 3 +- .../ca/allanwang/kau/ui/views/RippleCanvas.kt | 90 +++++----- .../kotlin/ca/allanwang/kau/utils/AnimUtils.kt | 5 +- .../kotlin/ca/allanwang/kau/utils/ColorUtils.kt | 15 ++ .../main/kotlin/ca/allanwang/kau/utils/Const.kt | 6 +- .../kotlin/ca/allanwang/kau/utils/Kotterknife.kt | 183 ++++++++++++++++++--- .../kotlin/ca/allanwang/kau/utils/PackageUtils.kt | 3 +- .../kotlin/ca/allanwang/kau/utils/ViewUtils.kt | 54 ++++-- .../main/kotlin/ca/allanwang/kau/xml/Changelog.kt | 103 ++++++++++++ core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt | 42 +++++ 13 files changed, 458 insertions(+), 200 deletions(-) delete mode 100644 core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt create mode 100644 core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt create mode 100644 core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt (limited to 'core/src/main/kotlin/ca/allanwang') diff --git a/core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt b/core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt deleted file mode 100644 index 43546ae..0000000 --- a/core/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt +++ /dev/null @@ -1,103 +0,0 @@ -package ca.allanwang.kau.changelog - -import android.content.Context -import android.content.res.XmlResourceParser -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 - - -/** - * 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>, @ColorInt val textColor: Int? = null) : RecyclerView.Adapter() { - - 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> { - val items = mutableListOf>() - 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>): 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/kotlin/LazyResettable.kt b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt index 2981dda..701cb07 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/kotlin/LazyResettable.kt @@ -13,7 +13,7 @@ internal object UNINITIALIZED fun lazyResettable(initializer: () -> T): LazyResettable = LazyResettable(initializer) -class LazyResettable(private val initializer: () -> T, lock: Any? = null) : ILazyResettable, Serializable { +open class LazyResettable(private val initializer: () -> T, lock: Any? = null) : ILazyResettable, Serializable { @Volatile private var _value: Any = UNINITIALIZED private val lock = lock ?: this @@ -52,4 +52,32 @@ class LazyResettable(private val initializer: () -> T, lock: Any? = nul interface ILazyResettable : Lazy { fun invalidate() +} + +interface ILazyResettableRegistry { + fun lazy(initializer: () -> T): LazyResettable + fun add(resettable: LazyResettable): LazyResettable + fun invalidateAll() + fun clear() +} + +/** + * The following below is a helper class that registers all resettables into a weakly held list + * All resettables can therefore be invalidated at once + */ +class LazyResettableRegistry : ILazyResettableRegistry { + + var lazyRegistry: MutableList> = mutableListOf() + + override fun lazy(initializer: () -> T): LazyResettable + = add(lazyResettable(initializer)) + + override fun add(resettable: LazyResettable): LazyResettable { + lazyRegistry.add(resettable) + return resettable + } + + override fun invalidateAll() = lazyRegistry.forEach { it.invalidate() } + + override fun clear() = lazyRegistry.clear() } \ 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 index fa6c5a9..c1ce282 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt @@ -6,6 +6,19 @@ import ca.allanwang.kau.kotlin.ILazyResettable /** * Created by Allan Wang on 2017-06-07. + * + * Base class for shared preferences + * All objects extending this class must be called in + * the app's [android.app.Application] class + * + * See the [KPref.kpref] extensions for more details + * + * Furthermore, all kprefs are held in the [prefMap], + * so if you wish to reset a preference, you must also invalidate the kpref + * from that map + * + * You may optionally override [deleteKeys]. This will be called on initialization + * And delete all keys returned from that method */ open class KPref { @@ -18,6 +31,12 @@ open class KPref { initialized = true this.c = c.applicationContext PREFERENCE_NAME = preferenceName + val toDelete = deleteKeys() + if (toDelete.isNotEmpty()) { + val edit = sp.edit() + toDelete.forEach { edit.remove(it) } + edit.apply() + } } internal val sp: SharedPreferences by lazy { @@ -33,4 +52,6 @@ open class KPref { operator fun get(key: String): ILazyResettable<*>? = prefMap[key] + open fun deleteKeys(): Array = arrayOf() + } \ 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 index f742078..8a582d8 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt @@ -2,7 +2,6 @@ package ca.allanwang.kau.kpref import ca.allanwang.kau.kotlin.ILazyResettable -object UNINITIALIZED fun KPref.kpref(key: String, fallback: Boolean, postSetter: (value: Boolean) -> Unit = {}) = KPrefDelegate(key, fallback, this, postSetter) fun KPref.kpref(key: String, fallback: Double, postSetter: (value: Float) -> Unit = {}) = KPrefDelegate(key, fallback.toFloat(), this, postSetter) @@ -25,6 +24,8 @@ class KPrefDelegate internal constructor( private val key: String, private val fallback: T, private val pref: KPref, var postSetter: (value: T) -> Unit = {}, lock: Any? = null ) : ILazyResettable, java.io.Serializable { + private object UNINITIALIZED + @Volatile private var _value: Any = UNINITIALIZED private val lock = lock ?: this diff --git a/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt b/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt index 773490c..f587e60 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/ui/views/RippleCanvas.kt @@ -1,14 +1,13 @@ package ca.allanwang.kau.ui.views +import android.animation.Animator +import android.animation.AnimatorListenerAdapter 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.graphics.* import android.util.AttributeSet import android.view.View -import ca.allanwang.kau.utils.adjustAlpha /** * Created by Allan Wang on 2016-11-17. @@ -21,63 +20,45 @@ import ca.allanwang.kau.utils.adjustAlpha class RippleCanvas @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { - private val paint: Paint = Paint() + private val paint: Paint = Paint().apply { + isAntiAlias = true + style = Paint.Style.FILL + } + private val eraser: Paint = Paint().apply { + style = Paint.Style.FILL + xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) + } private var baseColor = Color.TRANSPARENT private val ripples: MutableList = 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 + * Draw ripples one at a time in the order given + * To support transparent ripples, we simply erase the overlapping base before adding a new circle */ 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 + paint.color = baseColor + canvas.drawRect(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat(), paint) + val itr = ripples.iterator() + while (itr.hasNext()) { + val r = itr.next() + paint.color = r.color + canvas.drawCircle(r.x, r.y, r.radius, eraser) + canvas.drawCircle(r.x, r.y, r.radius, paint) + if (r.radius == r.maxRadius) { + itr.remove() + baseColor = r.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) { + fun ripple(color: Int, + startX: Float = 0f, + startY: Float = 0f, + duration: Long = 600L, + callback: (() -> Unit)? = null) { val w = width.toFloat() val h = height.toFloat() val x = when (startX) { @@ -91,7 +72,7 @@ class RippleCanvas @JvmOverloads constructor( 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) + val ripple = Ripple(color, x, y, 0f, maxRadius) ripples.add(ripple) val animator = ValueAnimator.ofFloat(0f, maxRadius) animator.duration = duration @@ -99,6 +80,11 @@ class RippleCanvas @JvmOverloads constructor( ripple.radius = animation.animatedValue as Float invalidate() } + if (callback != null) + animator.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationCancel(animation: Animator?) = callback() + override fun onAnimationEnd(animation: Animator?) = callback() + }) animator.start() } @@ -111,6 +97,8 @@ class RippleCanvas @JvmOverloads constructor( invalidate() } + override fun setBackgroundColor(color: Int) = set(color) + /** * Sets a color directly but with a transition */ @@ -129,12 +117,10 @@ class RippleCanvas @JvmOverloads constructor( val x: Float, val y: Float, var radius: Float, - val maxRadius: Float, - val fade: Boolean) + val maxRadius: Float) 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/utils/AnimUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt index 112c8ec..5da21bb 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt @@ -6,6 +6,7 @@ import android.annotation.SuppressLint import android.support.annotation.StringRes import android.view.View import android.view.ViewAnimationUtils +import android.view.ViewPropertyAnimator import android.view.animation.Animation import android.view.animation.AnimationUtils import android.widget.TextView @@ -123,4 +124,6 @@ import android.widget.TextView }) } -@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 +@KauUtils fun TextView.setTextWithFade(@StringRes textId: Int, duration: Long = 200, onFinish: (() -> Unit)? = null) = setTextWithFade(context.getString(textId), duration, onFinish) + +@KauUtils inline fun ViewPropertyAnimator.scaleXY(value: Float) = scaleX(value).scaleY(value) \ 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 index 50d117c..8537185 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt @@ -15,10 +15,25 @@ import android.support.v7.widget.AppCompatEditText import android.support.v7.widget.Toolbar import android.widget.* import com.afollestad.materialdialogs.R +import java.util.* /** * Created by Allan Wang on 2017-06-08. */ + +/** + * Generates a random opaque color + * Note that this is mainly for testing + * Should you require this method often, consider + * rewriting the method and storing the [Random] instance + * rather than generating one each time + */ +inline val rndColor: Int + get() { + val rnd = Random() + return Color.rgb(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256)) + } + inline val Int.isColorDark: Boolean get() = isColorDark(0.5f) diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt index f267a60..3e90926 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt @@ -9,6 +9,6 @@ const val KAU_LEFT = 1 const val KAU_TOP = 2 const val KAU_RIGHT = 4 const val KAU_BOTTOM = 8 -const val KAU_HORIZONTAL = KAU_LEFT and KAU_RIGHT -const val KAU_VERTICAL = KAU_TOP and KAU_BOTTOM -const val KAU_ALL = KAU_HORIZONTAL and KAU_VERTICAL \ No newline at end of file +const val KAU_HORIZONTAL = KAU_LEFT or KAU_RIGHT +const val KAU_VERTICAL = KAU_TOP or KAU_BOTTOM +const val KAU_ALL = KAU_HORIZONTAL or KAU_VERTICAL \ 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 index 3783931..f3c08bd 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt @@ -1,3 +1,5 @@ +@file:Suppress("UNCHECKED_CAST") + package ca.allanwang.kau.utils /** @@ -13,6 +15,7 @@ import android.app.DialogFragment import android.app.Fragment import android.support.v7.widget.RecyclerView.ViewHolder import android.view.View +import java.util.* import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty import android.support.v4.app.DialogFragment as SupportDialogFragment @@ -30,13 +33,13 @@ fun Dialog.bindView(id: Int) fun DialogFragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) -fun android.support.v4.app.DialogFragment.bindView(id: Int) +fun SupportDialogFragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) fun Fragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) -fun android.support.v4.app.Fragment.bindView(id: Int) +fun SupportFragment.bindView(id: Int) : ReadOnlyProperty = required(id, viewFinder) fun ViewHolder.bindView(id: Int) @@ -54,13 +57,13 @@ fun Dialog.bindOptionalView(id: Int) fun DialogFragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) -fun android.support.v4.app.DialogFragment.bindOptionalView(id: Int) +fun SupportDialogFragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) fun Fragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) -fun android.support.v4.app.Fragment.bindOptionalView(id: Int) +fun SupportFragment.bindOptionalView(id: Int) : ReadOnlyProperty = optional(id, viewFinder) fun ViewHolder.bindOptionalView(id: Int) @@ -78,13 +81,13 @@ fun Dialog.bindViews(vararg ids: Int) fun DialogFragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) -fun android.support.v4.app.DialogFragment.bindViews(vararg ids: Int) +fun SupportDialogFragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) fun Fragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) -fun android.support.v4.app.Fragment.bindViews(vararg ids: Int) +fun SupportFragment.bindViews(vararg ids: Int) : ReadOnlyProperty> = required(ids, viewFinder) fun ViewHolder.bindViews(vararg ids: Int) @@ -102,13 +105,13 @@ fun Dialog.bindOptionalViews(vararg ids: Int) fun DialogFragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) -fun android.support.v4.app.DialogFragment.bindOptionalViews(vararg ids: Int) +fun SupportDialogFragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) fun Fragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) -fun android.support.v4.app.Fragment.bindOptionalViews(vararg ids: Int) +fun SupportFragment.bindOptionalViews(vararg ids: Int) : ReadOnlyProperty> = optional(ids, viewFinder) fun ViewHolder.bindOptionalViews(vararg ids: Int) @@ -122,11 +125,11 @@ private inline val Dialog.viewFinder: Dialog.(Int) -> View? get() = { findViewById(it) } private inline val DialogFragment.viewFinder: DialogFragment.(Int) -> View? get() = { dialog.findViewById(it) } -private inline val android.support.v4.app.DialogFragment.viewFinder: android.support.v4.app.DialogFragment.(Int) -> View? +private inline val SupportDialogFragment.viewFinder: SupportDialogFragment.(Int) -> View? get() = { dialog.findViewById(it) } private inline val Fragment.viewFinder: Fragment.(Int) -> View? get() = { view.findViewById(it) } -private inline val android.support.v4.app.Fragment.viewFinder: android.support.v4.app.Fragment.(Int) -> View? +private inline val SupportFragment.viewFinder: SupportFragment.(Int) -> View? get() = { view!!.findViewById(it) } private inline val ViewHolder.viewFinder: ViewHolder.(Int) -> View? get() = { itemView.findViewById(it) } @@ -134,33 +137,173 @@ private inline val ViewHolder.viewFinder: ViewHolder.(Int) -> View? private fun viewNotFound(id: Int, desc: KProperty<*>): Nothing = throw IllegalStateException("View ID $id for '${desc.name}' not found.") -@Suppress("UNCHECKED_CAST") private fun 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 optional(id: Int, finder: T.(Int) -> View?) = Lazy { t: T, _ -> t.finder(id) as V? } -@Suppress("UNCHECKED_CAST") private fun 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 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(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty { - private object EMPTY +private open class Lazy(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty { + protected object EMPTY - private var value: Any? = EMPTY + protected var value: Any? = EMPTY override fun getValue(thisRef: T, property: KProperty<*>): V { - if (value == EMPTY) { + if (value == EMPTY) value = initializer(thisRef, property) - } - @Suppress("UNCHECKED_CAST") + return value as V } +} + +/* + * The components below are a variant of the view bindings with lazy resettables + * All bindings are weakly held so that they may be reset through KotterknifeRegistry.reset + * + * This is typically only needed in cases such as Fragments, + * where their lifecycle doesn't match that of an Activity or View + * + * Credits to MichaelRocks + */ + +fun View.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun Activity.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun Dialog.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun DialogFragment.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun SupportDialogFragment.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun Fragment.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun SupportFragment.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun ViewHolder.bindViewResettable(id: Int) + : ReadOnlyProperty = requiredResettable(id, viewFinder) + +fun View.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun Activity.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun Dialog.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun DialogFragment.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun SupportDialogFragment.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun Fragment.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun SupportFragment.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun ViewHolder.bindOptionalViewResettable(id: Int) + : ReadOnlyProperty = optionalResettable(id, viewFinder) + +fun View.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun Activity.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun Dialog.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun DialogFragment.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun SupportDialogFragment.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun Fragment.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun SupportFragment.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun ViewHolder.bindViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = requiredResettable(ids, viewFinder) + +fun View.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun Activity.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun Dialog.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun DialogFragment.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun SupportDialogFragment.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun Fragment.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun SupportFragment.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +fun ViewHolder.bindOptionalViewsResettable(vararg ids: Int) + : ReadOnlyProperty> = optionalResettable(ids, viewFinder) + +private fun requiredResettable(id: Int, finder: T.(Int) -> View?) + = LazyResettable { t: T, desc -> (t.finder(id) as V?)?.apply { } ?: viewNotFound(id, desc) } + +private fun optionalResettable(id: Int, finder: T.(Int) -> View?) + = LazyResettable { t: T, _ -> t.finder(id) as V? } + +private fun requiredResettable(ids: IntArray, finder: T.(Int) -> View?) + = LazyResettable { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } } + +private fun optionalResettable(ids: IntArray, finder: T.(Int) -> View?) + = LazyResettable { t: T, _ -> ids.map { t.finder(it) as V? }.filterNotNull() } + +//Like Kotterknife's lazy delegate but is resettable +private class LazyResettable(initializer: (T, KProperty<*>) -> V) : Lazy(initializer) { + override fun getValue(thisRef: T, property: KProperty<*>): V { + KotterknifeRegistry.register(thisRef!!, this) + return super.getValue(thisRef, property) + } + + fun reset() { + value = EMPTY + } +} + +object Kotterknife { + fun reset(target: Any) { + KotterknifeRegistry.reset(target) + } +} + +private object KotterknifeRegistry { + private val lazyMap = WeakHashMap>>() + + fun register(target: Any, lazy: LazyResettable<*, *>) + = lazyMap.getOrPut(target, { Collections.newSetFromMap(WeakHashMap()) }).add(lazy) + + fun reset(target: Any) = lazyMap[target]?.forEach { it.reset() } } \ 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 index 36bcc93..42d150e 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/PackageUtils.kt @@ -1,5 +1,6 @@ package ca.allanwang.kau.utils +import android.annotation.SuppressLint import android.content.Context import android.content.pm.PackageManager import android.os.Build @@ -31,11 +32,9 @@ import android.os.Build } inline val buildIsMarshmallowAndUp: Boolean - get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M inline val buildIsLollipopAndUp: Boolean - @SuppressWarnings("NewApi") get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP inline val buildIsNougatAndUp: Boolean diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt index ead2cb7..53d711d 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt @@ -3,20 +3,21 @@ package ca.allanwang.kau.utils import android.animation.ValueAnimator +import android.annotation.SuppressLint import android.content.Context import android.graphics.Color import android.os.Build -import android.support.annotation.* +import android.support.annotation.ColorInt +import android.support.annotation.ColorRes +import android.support.annotation.RequiresApi +import android.support.annotation.StringRes import android.support.design.widget.FloatingActionButton import android.support.design.widget.Snackbar import android.support.design.widget.TextInputEditText -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.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.inputmethod.InputMethodManager @@ -135,7 +136,6 @@ fun FloatingActionButton.hideIf(hide: Boolean) = if (hide) hide() else show() if (flag and KAU_RIGHT > 0) margin else p.rightMargin, if (flag and KAU_BOTTOM > 0) margin else p.bottomMargin ) - requestLayout() return true } @@ -184,7 +184,6 @@ fun FloatingActionButton.hideIf(hide: Boolean) = if (hide) hide() else show() if (flag and KAU_RIGHT > 0) padding else paddingRight, if (flag and KAU_BOTTOM > 0) padding else paddingBottom ) - requestLayout() } @KauUtils fun View.hideKeyboard() { @@ -222,23 +221,19 @@ fun Context.fullLinearRecycler(rvAdapter: RecyclerView.Adapter<*>? = null, confi } /** - * Animate a transition for a FloatinActionButton + * Animate a transition a given imageview * If it is not shown, the action will be invoked directly and the fab will be shown * If it is already shown, scaling and alpha animations will be added to the action */ -inline fun FloatingActionButton.transition(crossinline action: FloatingActionButton.() -> Unit) { - if (isHidden) { - action() - show() - } else { +inline fun T.fadeScaleTransition(duration: Long = 500L, minScale: Float = 0.7f, crossinline action: T.() -> Unit) { + if (!isVisible) action() + else { var transitioned = false ValueAnimator.ofFloat(1.0f, 0.0f, 1.0f).apply { - duration = 500L + this.duration = duration addUpdateListener { val x = it.animatedValue as Float - val scale = x * 0.3f + 0.7f - scaleX = scale - scaleY = scale + scaleXY = x * (1 - minScale) + minScale imageAlpha = (x * 255).toInt() if (it.animatedFraction > 0.5f && !transitioned) { transitioned = true @@ -266,4 +261,29 @@ fun FloatingActionButton.hideOnDownwardsScroll(recycler: RecyclerView) { else if (dy < 0 && isHidden) show() } }) +} + +inline var View.scaleXY + get() = Math.max(scaleX, scaleY) + set(value) { + scaleX = value + scaleY = value + } + +/** + * Creates an on touch listener that only emits on a short single tap + */ +@SuppressLint("ClickableViewAccessibility") +inline fun View.setOnSingleTapListener(crossinline onSingleTap: (v: View, event: MotionEvent) -> Unit) { + setOnTouchListener { v, event -> + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> true + MotionEvent.ACTION_UP -> { + if (event.eventTime - event.downTime < 100) + onSingleTap(v, event) + true + } + else -> false + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt b/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt new file mode 100644 index 0000000..4bf1836 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/xml/Changelog.kt @@ -0,0 +1,103 @@ +package ca.allanwang.kau.xml + +import android.content.Context +import android.content.res.XmlResourceParser +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 + + +/** + * 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>, @ColorInt val textColor: Int? = null) : RecyclerView.Adapter() { + + 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> { + val items = mutableListOf>() + 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>): 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/xml/FAQ.kt b/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt new file mode 100644 index 0000000..dedfbbf --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt @@ -0,0 +1,42 @@ +package ca.allanwang.kau.xml + +import android.content.Context +import android.content.res.XmlResourceParser +import android.support.annotation.XmlRes +import android.text.Html +import android.text.Spanned +import ca.allanwang.kau.utils.use +import org.xmlpull.v1.XmlPullParser + +/** + * Created by Allan Wang on 2017-07-30. + */ + +/** + * Parse an xml with two tags, Text and Text, + * and return them as a list of string pairs + */ +fun Context.kauParseFaq(@XmlRes xmlRes: Int, withNumbering: Boolean = true): List> { + val items = mutableListOf>() + resources.getXml(xmlRes).use { + parser: XmlResourceParser -> + var eventType = parser.eventType + var question: Spanned? = null + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + if (parser.name == "question") { + var q = parser.text.replace("\n", "
") + if (withNumbering) q = "${items.size + 1}. $q" + question = Html.fromHtml(q) + } else if (parser.name == "answer") { + items.add(Pair(question ?: throw IllegalArgumentException("KAU FAQ answer found without a question"), + Html.fromHtml(parser.text.replace("\n", "
")))) + question = null + } + } + + eventType = parser.next() + } + } + return items +} \ No newline at end of file -- cgit v1.2.3