From bafc1996d803862d30a2c7d0c402d30c79c4f647 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Thu, 3 Aug 2017 15:18:20 -0700 Subject: 3.2.2 - Create faq parser and update sample (#19) * Test emulator * Update readme * Update fastadapter and about listing * Make faq parser asynchronous * Modularize about panels * Add basis for faq * Test and finalize the faq panel * Update readme * Update changelog * Remove emulator for now * Update sample --- core/README.md | 10 +- .../kotlin/ca/allanwang/kau/xml/FaqTest.kt | 28 +++--- .../kau/ui/views/CollapsibleViewDelegate.kt | 105 +++++++++++++++++++++ .../main/kotlin/ca/allanwang/kau/utils/Const.kt | 7 +- .../kotlin/ca/allanwang/kau/utils/ContextUtils.kt | 9 +- core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt | 75 ++++++++------- core/src/main/res/values/ids.xml | 2 + 7 files changed, 188 insertions(+), 48 deletions(-) create mode 100644 core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt (limited to 'core') diff --git a/core/README.md b/core/README.md index b952797..385c7ed 100644 --- a/core/README.md +++ b/core/README.md @@ -10,6 +10,7 @@ * [Kotterknife](#kotterknife) * [Ripple Canvas](#ripple-canvas) * [MeasureSpecDelegate](#measure-spec-delegate) +* [CollapsibleViewDelegate](#collapsible-view-delegate) * [Timber Logger](#timber-logger) * [Email Builder](#email-builder) * [Extensions](#extensions) @@ -117,7 +118,7 @@ There is another parser for a FAQ list with the following format: This is an answer ``` -Calling `kauParseFaq` will give you a `List` that you can work with. +Call `kauParseFaq` and pass a callback taking in a `List` that you can work with. By default, the questions are numbered, and the content is formatted with HTML. You may still need to add your own methods to allow interaction with certain elements such as links. @@ -150,6 +151,13 @@ If you ever have a view needing exact aspect ratios with its parent and/or itsel Implementing this in any view class unlocks its attributes, giving you three layers of view measuring to ensure exact sizing. More information can be found in the [klass file](https://github.com/AllanWang/KAU/blob/master/core/src/main/kotlin/ca/allanwang/kau/ui/views/MeasureSpecDelegate.kt) +< a name="collapsible-view-delegate"> +## Collapsible View Delegate + +A common animation is having a view that can smoothly enter and exit by changing its height. +This delegate will implement everything for you and give you the methods `expand`, `collapse`, etc. +See the [kclass file](https://github.com/AllanWang/KAU/blob/master/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt) for more details. + ## Timber Logger diff --git a/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt b/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt index 94d1330..1b185f3 100644 --- a/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt +++ b/core/src/androidTest/kotlin/ca/allanwang/kau/xml/FaqTest.kt @@ -17,22 +17,26 @@ class FaqTest { @Test fun simpleTest() { - val data = InstrumentationRegistry.getTargetContext().kauParseFaq(R.xml.test_faq) - assertEquals(2, data.size, "FAQ size is incorrect") - assertEquals("1. This is a question", data.first().first.toString(), "First question does not match") - assertEquals("This is an answer", data.first().second.toString(), "First answer does not match") - assertEquals("2. This is another question", data.last().first.toString(), "Second question does not match") - assertEquals("This is another answer", data.last().second.toString(), "Second answer does not match") + InstrumentationRegistry.getTargetContext().kauParseFaq(R.xml.test_faq) { + data -> + assertEquals(2, data.size, "FAQ size is incorrect") + assertEquals("1. This is a question", data.first().first.toString(), "First question does not match") + assertEquals("This is an answer", data.first().second.toString(), "First answer does not match") + assertEquals("2. This is another question", data.last().first.toString(), "Second question does not match") + assertEquals("This is another answer", data.last().second.toString(), "Second answer does not match") + } } @Test fun withoutNumbering() { - val data = InstrumentationRegistry.getTargetContext().kauParseFaq(R.xml.test_faq, false) - assertEquals(2, data.size, "FAQ size is incorrect") - assertEquals("This is a question", data.first().first.toString(), "First question does not match") - assertEquals("This is an answer", data.first().second.toString(), "First answer does not match") - assertEquals("This is another question", data.last().first.toString(), "Second question does not match") - assertEquals("This is another answer", data.last().second.toString(), "Second answer does not match") + InstrumentationRegistry.getTargetContext().kauParseFaq(R.xml.test_faq, false) { + data -> + assertEquals(2, data.size, "FAQ size is incorrect") + assertEquals("This is a question", data.first().first.toString(), "First question does not match") + assertEquals("This is an answer", data.first().second.toString(), "First answer does not match") + assertEquals("This is another question", data.last().first.toString(), "Second question does not match") + assertEquals("This is another answer", data.last().second.toString(), "Second answer does not match") + } } } \ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt b/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt new file mode 100644 index 0000000..6994ca2 --- /dev/null +++ b/core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt @@ -0,0 +1,105 @@ +package ca.allanwang.kau.ui.views + +import android.animation.ValueAnimator +import android.view.View +import ca.allanwang.kau.utils.* +import java.lang.ref.WeakReference + +/** + * Created by Allan Wang on 2017-08-03. + * + * Delegation class for collapsible views + * + * Views that implement this MUST call [initCollapsible] before using any of the methods + * Additionally, you will need to call [getCollapsibleDimension] and use the response for + * [View.setMeasuredDimension] during [View.onMeasure] + * (That method is protected so we cannot access it here) + * + * With reference to ExpandableLayout + */ +interface CollapsibleView { + var expansion: Float + val state: Int + val expanded: Boolean + fun initCollapsible(view: View) + fun resetCollapsibleAnimation() + fun getCollapsibleDimension(): Pair + fun toggleExpansion() + fun toggleExpansion(animate: Boolean) + fun expand() + fun expand(animate: Boolean) + fun collapse() + fun collapse(animate: Boolean) + fun setExpanded(expand: Boolean) + fun setExpanded(expand: Boolean, animate: Boolean) +} + +class CollapsibleViewDelegate : CollapsibleView { + + private lateinit var viewRef: WeakReference + private val view + get() = viewRef.get() + private var animator: ValueAnimator? = null + + override var expansion = 0f + set(value) { + if (value == field) return + var v = value + if (v > 1) v = 1f + else if (v < 0) v = 0f + stateHolder = + if (v == 0f) KAU_COLLAPSED + else if (v == 1f) KAU_EXPANDED + else if (v - field < 0) KAU_COLLAPSING + else KAU_EXPANDING + field = v + view?.goneIf(state == KAU_COLLAPSED) + view?.requestLayout() + } + + private var stateHolder = KAU_COLLAPSED + override val state + get() = stateHolder + override val expanded + get() = stateHolder == KAU_EXPANDED || stateHolder == KAU_EXPANDING + + override fun initCollapsible(view: View) { + this.viewRef = WeakReference(view) + } + + override fun resetCollapsibleAnimation() { + animator?.cancel() + animator = null + if (stateHolder == KAU_COLLAPSING) stateHolder = KAU_COLLAPSED + else if (stateHolder == KAU_EXPANDING) stateHolder = KAU_EXPANDED + } + + override fun getCollapsibleDimension(): Pair { + val v = view ?: return Pair(0, 0) + val size = v.measuredHeight + v.goneIf(expansion == 0f && size == 0) + return Pair(v.measuredWidth, Math.round(size * expansion)) + } + + private fun animateSize(target: Float) { + resetCollapsibleAnimation() + animator = ValueAnimator.ofFloat(expansion, target).apply { + addUpdateListener { expansion = it.animatedValue as Float } + start() + } + } + + override fun toggleExpansion() = toggleExpansion(true) + override fun toggleExpansion(animate: Boolean) = setExpanded(!expanded, animate) + override fun expand() = expand(true) + override fun expand(animate: Boolean) = setExpanded(true, animate) + override fun collapse() = collapse(true) + override fun collapse(animate: Boolean) = setExpanded(false, animate) + override fun setExpanded(expand: Boolean) = setExpanded(expand, true) + override fun setExpanded(expand: Boolean, animate: Boolean) { + if (expand == expanded) return //state already matches + val target = if (expand) 1f else 0f + if (animate) animateSize(target) else expansion = target + } + +} \ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt index 3e90926..dad01f1 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/Const.kt @@ -11,4 +11,9 @@ const val KAU_RIGHT = 4 const val KAU_BOTTOM = 8 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 +const val KAU_ALL = KAU_HORIZONTAL or KAU_VERTICAL + +const val KAU_COLLAPSED = 0 +const val KAU_COLLAPSING = 1 +const val KAU_EXPANDING = 2 +const val KAU_EXPANDED = 3 \ No newline at end of file diff --git a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt index 2219b5d..20cec73 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt @@ -1,3 +1,5 @@ +@file:Suppress("NOTHING_TO_INLINE") + package ca.allanwang.kau.utils import android.annotation.SuppressLint @@ -89,9 +91,12 @@ fun Context.startLink(vararg url: String?) { } //Toast helpers -fun Context.toast(@StringRes id: Int, duration: Int = Toast.LENGTH_LONG) = toast(this.string(id), duration) +inline fun View.toast(@StringRes id: Int, duration: Int = Toast.LENGTH_LONG) = context.toast(id, duration) + +inline fun Context.toast(@StringRes id: Int, duration: Int = Toast.LENGTH_LONG) = toast(this.string(id), duration) -fun Context.toast(text: String, duration: Int = Toast.LENGTH_LONG) { +inline fun View.toast(text: String, duration: Int = Toast.LENGTH_LONG) = context.toast(text, duration) +inline fun Context.toast(text: String, duration: Int = Toast.LENGTH_LONG) { Toast.makeText(this, text, duration).show() } diff --git a/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt b/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt index b39540c..07a0287 100644 --- a/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt +++ b/core/src/main/kotlin/ca/allanwang/kau/xml/FAQ.kt @@ -6,6 +6,8 @@ import android.support.annotation.XmlRes import android.text.Html import android.text.Spanned import ca.allanwang.kau.utils.use +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.uiThread import org.xmlpull.v1.XmlPullParser /** @@ -13,42 +15,51 @@ import org.xmlpull.v1.XmlPullParser */ /** - * Parse an xml with two tags, Text and Text, - * and return them as a list of string pairs + * Parse an xml asynchronously with two tags, Text and Text, + * and invoke the [callback] on the ui thread */ -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 - var flag = -1 //-1, 0, 1 -> invalid, question, answer - while (eventType != XmlPullParser.END_DOCUMENT) { - if (eventType == XmlPullParser.START_TAG) { - flag = when (parser.name) { - "question" -> 0 - "answer" -> 1 - else -> -1 - } - } else if (eventType == XmlPullParser.TEXT) { - when (flag) { - 0 -> { - var q = parser.text.replace("\n", "
") - if (withNumbering) q = "${items.size + 1}. $q" - question = Html.fromHtml(q) - flag = -1 +@Suppress("DEPRECATION") +fun Context.kauParseFaq( + @XmlRes xmlRes: Int, + /** + * If \n is used, it will automatically be converted to
+ */ + parseNewLine: Boolean = true, + callback: (items: List) -> Unit) { + doAsync { + val items = mutableListOf() + resources.getXml(xmlRes).use { + parser: XmlResourceParser -> + var eventType = parser.eventType + var question: Spanned? = null + var flag = -1 //-1, 0, 1 -> invalid, question, answer + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + flag = when (parser.name) { + "question" -> 0 + "answer" -> 1 + else -> -1 } - 1 -> { - items.add(Pair(question ?: throw IllegalArgumentException("KAU FAQ answer found without a question"), - Html.fromHtml(parser.text.replace("\n", "
")))) - question = null - flag = -1 + } else if (eventType == XmlPullParser.TEXT) { + when (flag) { + 0 -> { + question = Html.fromHtml(parser.text.replace("\n", if (parseNewLine) "
" else "")) + flag = -1 + } + 1 -> { + items.add(FaqItem(items.size + 1, + question ?: throw IllegalArgumentException("KAU FAQ answer found without a question"), + Html.fromHtml(parser.text.replace("\n", if (parseNewLine) "
" else "")))) + question = null + flag = -1 + } } } + eventType = parser.next() } - - eventType = parser.next() } + uiThread { callback(items) } } - return items -} \ No newline at end of file +} + +data class FaqItem(val number: Int, val question: Spanned, val answer: Spanned) \ No newline at end of file diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml index 003e8a7..c4912e2 100644 --- a/core/src/main/res/values/ids.xml +++ b/core/src/main/res/values/ids.xml @@ -6,6 +6,8 @@ + + -- cgit v1.2.3