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 #5039 : Align policy text and symbols to the left, partial fix for list items #5573

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,6 +12,7 @@ import org.oppia.android.app.model.PoliciesFragmentArguments
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.locale.LeftAlignedSymbolsSpan
import org.oppia.android.util.parser.html.HtmlParser
import org.oppia.android.util.parser.html.PolicyType
import javax.inject.Inject
Expand Down Expand Up @@ -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 = "",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.oppia.android.util.locale

import android.graphics.Canvas
import android.graphics.Paint
import android.text.style.ReplacementSpan

// Custom span to force LTR alignment for symbols as well
// Custom span to force LTR alignment for all text including symbols
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a KDOC rather than a comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import android.text.style.LeadingMarginSpan
import androidx.core.view.ViewCompat
import org.oppia.android.util.R
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.

Expand Down Expand Up @@ -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 = (16f * context.resources.displayMetrics.density).toInt()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here and below, why are we using a hardcoded value of 16f here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apologies for the oversight. I’ve replaced the hardcoded value to improve maintainability.

For Unordered List (UL):

val baseMargin = context.resources.getDimensionPixelSize(R.dimen.spacing_before_bullet)

For Ordered List (OL):

val baseMargin = context.resources.getDimensionPixelSize(R.dimen.spacing_before_number_prefix)


private val isRtl by lazy {
displayLocale.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL
}
private val clipBounds by lazy { Rect() }

override fun drawLeadingMargin(
canvas: Canvas,
Expand All @@ -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.
Expand Down Expand Up @@ -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 <ol> tags. */
Expand All @@ -123,17 +119,11 @@ sealed class ListItemLeadingMarginSpan : LeadingMarginSpan {
private val displayLocale: OppiaLocale.DisplayLocale
) : ListItemLeadingMarginSpan() {
private val resources = context.resources
private val spacingBeforeText = resources.getDimensionPixelSize(R.dimen.spacing_before_text)
private val spacingBeforeNumberPrefix =
resources.getDimensionPixelSize(R.dimen.spacing_before_number_prefix)
private val baseMargin = (16f * context.resources.displayMetrics.density).toInt()

// Try to use a computed margin, but otherwise guess if there's no guaranteed spacing.
private var computedLeadingMargin =
2 * longestNumberedItemPrefix.length + spacingBeforeText

private val isRtl by lazy {
displayLocale.getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL
}
2 * longestNumberedItemPrefix.length + baseMargin

override fun drawLeadingMargin(
canvas: Canvas,
Expand All @@ -153,30 +143,24 @@ sealed class ListItemLeadingMarginSpan : LeadingMarginSpan {
val isFirstCharacter = startCharOfSpan == start

if (isFirstCharacter) {
// Force left alignment
paint.textAlign = Paint.Align.LEFT

val textWidth = Rect().also {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this variable is nolonger used in the new code.

Copy link
Contributor Author

@TanishMoral11 TanishMoral11 Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @adhiamboperes , I’ve removed this unused variable.

paint.getTextBounds(
numberedItemPrefix, /* start= */ 0, /* end= */ numberedItemPrefix.length, it
)
}.width()
val longestTextWidth = Rect().also {
paint.getTextBounds(
longestNumberedItemPrefix,
/* start= */ 0,
/* end= */ longestNumberedItemPrefix.length,
it
)
}.width()
computedLeadingMargin = longestTextWidth + spacingBeforeNumberPrefix + spacingBeforeText

// Compute the prefix's start x value such that it is right-aligned with other numbers in
// the list.
val indentedX = parentAbsoluteLeadingMargin + spacingBeforeNumberPrefix
val endAlignedX = (max(textWidth, longestTextWidth) - textWidth) + indentedX
val prefixStartX = if (isRtl) canvas.width - endAlignedX - 1 else endAlignedX
canvas.drawText(numberedItemPrefix, prefixStartX.toFloat(), baseline.toFloat(), paint)

// Positioning calculation
val prefixStartX = x.toFloat() + baseMargin

// Draw the numbered prefix
canvas.drawText(numberedItemPrefix, prefixStartX, baseline.toFloat(), paint)
}
}

override fun getLeadingMargin(first: Boolean) = computedLeadingMargin
override fun getLeadingMargin(first: Boolean) =
baseMargin * 2
}
}
Loading