Skip to content

Commit

Permalink
Fix #456: Improvised bullet code (#490)
Browse files Browse the repository at this point in the history
* Improvised bullet code

* Updated bullets

* Fixes for MDPI devices

* EOF added

* Added yoffset in bullets

* Nit changes

* Directly added context instead of activity

* Added test cases

* Nit changes

* Added TODO
  • Loading branch information
rt4914 authored Dec 18, 2019
1 parent e3c6553 commit b5e33de
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 15 deletions.
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>

0 comments on commit b5e33de

Please sign in to comment.