Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #456: Improvised bullet code #490

Merged
merged 13 commits into from
Dec 18, 2019
36 changes: 31 additions & 5 deletions app/src/sharedTest/java/org/oppia/app/parser/HtmlParserTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap
import android.text.Spannable
import android.widget.TextView
Expand Down Expand Up @@ -39,6 +40,7 @@ import org.oppia.util.logging.EnableConsoleLog
import org.oppia.util.logging.EnableFileLog
import org.oppia.util.logging.GlobalLogLevel
import org.oppia.util.logging.LogLevel
import org.oppia.util.parser.CustomBulletSpan
import org.oppia.util.parser.DefaultGcsPrefix
import org.oppia.util.parser.DefaultGcsResource
import org.oppia.util.parser.GlideImageLoader
Expand All @@ -57,8 +59,7 @@ import javax.inject.Singleton
class HtmlParserTest {

private lateinit var launchedActivity: Activity
@Inject
lateinit var htmlParserFactory: HtmlParser.Factory
@Inject lateinit var htmlParserFactory: HtmlParser.Factory

@get:Rule
var activityTestRule: ActivityTestRule<HtmlParserTestActivity> = ActivityTestRule(
Expand All @@ -85,7 +86,7 @@ class HtmlParserTest {
fun tearDown() {
Intents.release()
}

private fun setUpTestApplicationComponent() {
DaggerHtmlParserTest_TestApplicationComponent.builder()
.setApplication(ApplicationProvider.getApplicationContext())
Expand Down Expand Up @@ -128,14 +129,39 @@ class HtmlParserTest {
onView(withId(R.id.test_html_content_text_view)).check(matches(not(textView.text.toString())))
}

@Test
fun testHtmlContent_customSpan_isAdded() {
val textView = activityTestRule.activity.findViewById(R.id.test_html_content_text_view) as TextView
val htmlParser = htmlParserFactory.create(/* entityType= */ "", /* entityId= */ "", /* imageCenterAlign= */ true)
val htmlResult: Spannable = htmlParser.parseOppiaHtml(
"<p>You should know the following before going on:<br></p>" +
"<ul><li>The counting numbers (1, 2, 3, 4, 5 ….)<br></li>" +
"<li>How to tell whether one counting number is bigger or smaller than another<br></li></ul>",
textView
)

/* Reference: https://medium.com/androiddevelopers/spantastic-text-styling-with-spans-17b0c16b4568#e345 */
val bulletSpans = htmlResult.getSpans<CustomBulletSpan>(0, htmlResult.length, CustomBulletSpan::class.java)
assertThat(bulletSpans.size.toLong()).isEqualTo(2)

val bulletSpan0 = bulletSpans[0] as CustomBulletSpan
assertThat(bulletSpan0).isNotNull()

val bulletSpan1 = bulletSpans[1] as CustomBulletSpan
assertThat(bulletSpan1).isNotNull()
}

class FakeImageLoader : ImageLoader {
override fun load(imageUrl: String, target: CustomTarget<Bitmap>) {

}
}

@Qualifier
annotation class TestDispatcher
private fun getResources(): Resources {
return ApplicationProvider.getApplicationContext<Context>().resources
}

@Qualifier annotation class TestDispatcher

// TODO(#89): Move this to a common test application component.
@Module
Expand Down
77 changes: 77 additions & 0 deletions utility/src/main/java/org/oppia/util/parser/CustomBulletSpan.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.oppia.util.parser

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.Path
import android.graphics.Path.Direction
import android.text.Layout
import android.text.Spanned
import android.text.style.LeadingMarginSpan
import org.oppia.util.R

// TODO(#562): Add screenshot tests to check whether the drawing logic works correctly on all devices.

/**
* Copy of [android.text.style.BulletSpan] from android SDK 28 with removed internal code.
* This class helps us to customise bullet radius, gap width and offset present in rich-text.
* Reference: https://github.com/davidbilik/bullet-span-sample
*/
class CustomBulletSpan(context: Context) : LeadingMarginSpan {
private var bulletRadius: Int = 0
private var gapWidth: Int = 0
private var yOffset: Int = 0
private var bulletLeadingMargin: Int = 0

init {
bulletRadius = context.resources.getDimensionPixelSize(R.dimen.bullet_radius)
gapWidth = context.resources.getDimensionPixelSize(R.dimen.bullet_gap_width)
yOffset = context.resources.getDimensionPixelSize(R.dimen.bullet_y_offset)
bulletLeadingMargin = context.resources.getDimensionPixelSize(R.dimen.bullet_leading_margin)
}

private var mBulletPath: Path? = null

override fun getLeadingMargin(first: Boolean): Int {
return bulletLeadingMargin
}

override fun drawLeadingMargin(
canvas: Canvas, paint: Paint, x: Int, dir: Int,
top: Int, baseline: Int, bottom: Int,
text: CharSequence, start: Int, end: Int,
first: Boolean,
layout: Layout?
) {
if ((text as Spanned).getSpanStart(this) == start) {
val style = paint.style
paint.style = Paint.Style.FILL

var yPosition = if (layout != null) {
val line = layout.getLineForOffset(start)
layout.getLineBaseline(line).toFloat() - bulletRadius * 2f
} else {
(top + bottom) / 2f
}
yPosition += yOffset

val xPosition = (x + dir * bulletRadius).toFloat()

if (canvas.isHardwareAccelerated) {
if (mBulletPath == null) {
mBulletPath = Path()
mBulletPath!!.addCircle(0.0f, 0.0f, bulletRadius.toFloat(), Direction.CW)
}

canvas.save()
canvas.translate(xPosition, yPosition)
canvas.drawPath(mBulletPath!!, paint)
canvas.restore()
} else {
canvas.drawCircle(xPosition, yPosition, bulletRadius.toFloat(), paint)
}

paint.style = style
}
}
}
33 changes: 23 additions & 10 deletions utility/src/main/java/org/oppia/util/parser/HtmlParser.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package org.oppia.util.parser

import android.os.Build
import android.text.Html
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.BulletSpan
import android.widget.TextView
import javax.inject.Inject

Expand Down Expand Up @@ -44,18 +47,28 @@ class HtmlParser private constructor(
}

val imageGetter = urlImageParserFactory.create(htmlContentTextView, entityType, entityId, imageCenterAlign)
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
trimSpannable(
Html.fromHtml(
htmlContent,
Html.FROM_HTML_MODE_LEGACY,
imageGetter, /* tagHandler= */
null
) as SpannableStringBuilder
)

@Suppress("DEPRECATION")
val htmlSpannable = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Html.fromHtml(htmlContent, Html.FROM_HTML_MODE_LEGACY, imageGetter, LiTagHandler())
} else {
trimSpannable(Html.fromHtml(htmlContent, imageGetter, /* tagHandler= */ null) as SpannableStringBuilder)
Html.fromHtml(htmlContent, imageGetter, LiTagHandler())
}

val spannableBuilder = SpannableStringBuilder(htmlSpannable)
val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java)
bulletSpans.forEach {
val start = spannableBuilder.getSpanStart(it)
val end = spannableBuilder.getSpanEnd(it)
spannableBuilder.removeSpan(it)
spannableBuilder.setSpan(
CustomBulletSpan(htmlContentTextView.context),
start,
end,
Spanned.SPAN_INCLUSIVE_EXCLUSIVE
)
}
return trimSpannable(spannableBuilder)
}

private fun trimSpannable(spannable: SpannableStringBuilder): SpannableStringBuilder {
Expand Down
37 changes: 37 additions & 0 deletions utility/src/main/java/org/oppia/util/parser/LiTagHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.oppia.util.parser

import android.text.Editable
import android.text.Html
import android.text.Spannable
import android.text.Spanned
import android.text.style.BulletSpan
import org.xml.sax.XMLReader

/**
* [Html.TagHandler] implementation that processes <li> tags and creates bullets.
*
* Reference: https://github.com/davidbilik/bullet-span-sample
*/
class LiTagHandler : Html.TagHandler {
/**
* Helper marker class. Based on [Html.fromHtml] implementation.
*/
class Bullet

override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) {
if (tag == "li" && opening) {
output.setSpan(Bullet(), output.length, output.length, Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
}
if (tag == "li" && !opening) {
output.append("<br>")
val lastMark = output.getSpans(0, output.length, Bullet::class.java).lastOrNull()
lastMark?.let {
val start = output.getSpanStart(it)
output.removeSpan(it)
if (start != output.length) {
output.setSpan(BulletSpan(), start, output.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
}
}
}
}
7 changes: 7 additions & 0 deletions utility/src/main/res/values/dimens.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="bullet_radius">4dp</dimen>
<dimen name="bullet_gap_width">16dp</dimen>
<dimen name="bullet_y_offset">2dp</dimen>
<dimen name="bullet_leading_margin">24dp</dimen>
</resources>