diff --git a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt index 1b2bcb20a3d..59fe472a3c7 100644 --- a/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt +++ b/app/src/main/java/org/oppia/android/app/policies/PoliciesFragmentPresenter.kt @@ -1,5 +1,7 @@ package org.oppia.android.app.policies +import android.text.SpannableString +import android.text.Spanned import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -11,6 +13,7 @@ import org.oppia.android.app.model.PolicyPage import org.oppia.android.app.translation.AppLanguageResourceHandler import org.oppia.android.databinding.PoliciesFragmentBinding import org.oppia.android.util.parser.html.HtmlParser +import org.oppia.android.util.parser.html.LeftAlignedSymbolsSpan import org.oppia.android.util.parser.html.PolicyType import javax.inject.Inject @@ -53,16 +56,39 @@ class PoliciesFragmentPresenter @Inject constructor( policyWebLink = resourceHandler.getStringInLocale(R.string.terms_of_service_web_link) } - binding.policyDescriptionTextView.text = htmlParserFactory.create( + val parsedHtmlDescription = htmlParserFactory.create( policyOppiaTagActionListener = this, displayLocale = resourceHandler.getDisplayLocale() ).parseOppiaHtml( - policyDescription, - binding.policyDescriptionTextView, + rawString = policyDescription, + htmlContentTextView = binding.policyDescriptionTextView, supportsLinks = true, supportsConceptCards = false ) + binding.policyDescriptionTextView.apply { + layoutDirection = View.LAYOUT_DIRECTION_LTR + textAlignment = View.TEXT_ALIGNMENT_TEXT_START + textDirection = View.TEXT_DIRECTION_LTR + setSingleLine(false) + setMaxLines(Int.MAX_VALUE) + } + + val spannableString = SpannableString(parsedHtmlDescription) + parsedHtmlDescription.split("\n").forEachIndexed { lineIndex, line -> + val lineStart = parsedHtmlDescription.indexOf(line) + if (line.trimStart().startsWith("•")) { + val bulletIndex = lineStart + line.indexOf("•") + spannableString.setSpan( + LeftAlignedSymbolsSpan(), + bulletIndex, + bulletIndex + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + binding.policyDescriptionTextView.text = spannableString + binding.policyWebLinkTextView.text = htmlParserFactory.create( gcsResourceName = "", entityType = "", diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel b/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel index 959a1c43f69..5177894544f 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel +++ b/utility/src/main/java/org/oppia/android/util/parser/html/BUILD.bazel @@ -46,6 +46,7 @@ kt_android_library( name = "list_item_leading_margin_span", srcs = [ "ListItemLeadingMarginSpan.kt", + "LeftAlignedSymbolsSpan.kt", ], visibility = [ "//app:__subpackages__", diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/LeftAlignedSymbolsSpan.kt b/utility/src/main/java/org/oppia/android/util/parser/html/LeftAlignedSymbolsSpan.kt new file mode 100644 index 00000000000..3e9365c409f --- /dev/null +++ b/utility/src/main/java/org/oppia/android/util/parser/html/LeftAlignedSymbolsSpan.kt @@ -0,0 +1,42 @@ +package org.oppia.android.util.parser.html + +import android.graphics.Canvas +import android.graphics.Paint +import android.text.style.ReplacementSpan + +/** + * Custom span to force LTR (left-to-right) alignment for all text, + * including symbols, regardless of the system's text direction. + */ +class LeftAlignedSymbolsSpan : ReplacementSpan() { + override fun getSize( + paint: Paint, + text: CharSequence?, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + return paint.measureText(text, start, end).toInt() + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + val originalAlignment = paint.textAlign + paint.textAlign = Paint.Align.LEFT + + // Draw the bullet point at the exact x position + canvas.drawText(text, start, end, x, y.toFloat(), paint) + + // Restore original alignment + paint.textAlign = originalAlignment + } +} diff --git a/utility/src/main/java/org/oppia/android/util/parser/html/ListItemLeadingMarginSpan.kt b/utility/src/main/java/org/oppia/android/util/parser/html/ListItemLeadingMarginSpan.kt index de1c028aed2..27fa8b5b8fb 100644 --- a/utility/src/main/java/org/oppia/android/util/parser/html/ListItemLeadingMarginSpan.kt +++ b/utility/src/main/java/org/oppia/android/util/parser/html/ListItemLeadingMarginSpan.kt @@ -3,15 +3,14 @@ package org.oppia.android.util.parser.html import android.content.Context import android.graphics.Canvas import android.graphics.Paint -import android.graphics.Rect import android.graphics.RectF import android.text.Layout import android.text.Spanned import android.text.style.LeadingMarginSpan import androidx.core.view.ViewCompat import org.oppia.android.util.R +import org.oppia.android.util.R.dimen.spacing_before_bullet import org.oppia.android.util.locale.OppiaLocale -import kotlin.math.max // TODO(#562): Add screenshot tests to check whether the drawing logic works correctly on all devices. @@ -39,14 +38,13 @@ sealed class ListItemLeadingMarginSpan : LeadingMarginSpan { ) : ListItemLeadingMarginSpan() { private val resources = context.resources private val bulletRadius = resources.getDimensionPixelSize(R.dimen.bullet_radius) - private val spacingBeforeText = resources.getDimensionPixelSize(R.dimen.spacing_before_text) - private val spacingBeforeBullet = resources.getDimensionPixelSize(R.dimen.spacing_before_bullet) private val bulletDiameter by lazy { bulletRadius * 2 } + private val baseMargin = context.resources.getDimensionPixelSize((spacing_before_bullet)) + private val isRtl by lazy { displayLocale.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL } - private val clipBounds by lazy { Rect() } override fun drawLeadingMargin( canvas: Canvas, @@ -69,16 +67,14 @@ sealed class ListItemLeadingMarginSpan : LeadingMarginSpan { val previousStyle = paint.style val bulletDrawRadius = bulletRadius.toFloat() - val indentedX = parentAbsoluteLeadingMargin + spacingBeforeBullet - val bulletCenterLtrX = indentedX + bulletDrawRadius - val bulletCenterX = if (isRtl) { - // See https://stackoverflow.com/a/21845993/3689782 for 'right' property exclusivity. - val maxDrawX = if (canvas.getClipBounds(clipBounds)) { - clipBounds.right - 1 - } else canvas.width - 1 - maxDrawX - bulletCenterLtrX - } else bulletCenterLtrX + // Force left alignment + paint.textAlign = Paint.Align.LEFT + + // Positioning calculation + val bulletCenterLtrX = x.toFloat() + baseMargin * (indentationLevel + 1) + val bulletCenterX = bulletCenterLtrX val bulletCenterY = (top + bottom) / 2f + when (indentationLevel) { 0 -> { // A solid circle is used for the outermost bullet. @@ -111,7 +107,7 @@ sealed class ListItemLeadingMarginSpan : LeadingMarginSpan { } override fun getLeadingMargin(first: Boolean) = - bulletDiameter + spacingBeforeBullet + spacingBeforeText + baseMargin * (indentationLevel + 2) } /** A subclass of [LeadingMarginSpan] that shows nested list span for