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 --- .../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 + 6 files changed, 179 insertions(+), 47 deletions(-) create mode 100644 core/src/main/kotlin/ca/allanwang/kau/ui/views/CollapsibleViewDelegate.kt (limited to 'core/src') 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