Skip to content
Snippets Groups Projects
Commit 883a0769 authored by Taras's avatar Taras
Browse files

Merge branch 'feature/show_more_text_post' into develop

parents 4153dc00 a734a075
No related branches found
No related tags found
No related merge requests found
package org.futo.circles.view.read_more
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Rect
import android.graphics.Typeface
import android.text.Layout
import android.text.TextPaint
import android.text.TextUtils
import android.text.style.ClickableSpan
import android.text.style.TextAppearanceSpan
import android.util.AttributeSet
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.content.res.use
import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
import org.futo.circles.R
import kotlin.text.Typography.ellipsis
import kotlin.text.Typography.nbsp
class ReadMoreTextView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = R.attr.readMoreTextViewStyle
) : AppCompatTextView(context, attrs, defStyleAttr) {
private var readMoreMaxLines: Int = 2
private var readMoreText: String? = null
private var readMoreTextColor: ColorStateList? = null
private var bufferType: BufferType? = null
private var expanded: Boolean = false
private var originalText: CharSequence? = null
private var collapseText: CharSequence? = null
init {
context.obtainStyledAttributes(
attrs, R.styleable.ReadMoreTextView, defStyleAttr, 0
).use { ta ->
readMoreMaxLines = ta.getInt(
R.styleable.ReadMoreTextView_readMoreMaxLines,
readMoreMaxLines
)
readMoreText = ta.getString(R.styleable.ReadMoreTextView_readMoreText)
?.replace(' ', nbsp)
readMoreTextColor = ta.getColorStateList(
R.styleable.ReadMoreTextView_readMoreTextColor
) ?: readMoreTextColor
}
if (hasOnClickListeners()) throw IllegalStateException("Custom onClickListener not supported")
super.setOnClickListener { toggle() }
if (originalText != null) invalidateText()
}
override fun setLines(lines: Int) {
throw IllegalStateException("Use the app:readMoreMaxLines")
}
override fun setMaxLines(maxLines: Int) {
throw IllegalStateException("Use the app:readMoreMaxLines")
}
override fun setEllipsize(where: TextUtils.TruncateAt?) {
throw IllegalStateException("Not supported")
}
override fun setOnClickListener(l: OnClickListener?) {
throw IllegalStateException("Not supported")
}
private fun toggle() {
setExpanded(!expanded)
}
private fun setExpanded(expanded: Boolean) {
if (this.expanded != expanded) {
this.expanded = expanded
invalidateText()
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (w != oldw) {
originalText?.let { originalText ->
updateText(originalText, w)
}
}
}
override fun setText(text: CharSequence?, type: BufferType?) {
this.originalText = text
this.bufferType = type
updateText(text ?: "", width)
}
private fun updateText(text: CharSequence, width: Int) {
val maximumTextWidth = width - (paddingLeft + paddingRight)
val readMoreMaxLines = readMoreMaxLines
if (maximumTextWidth > 0 && readMoreMaxLines > 0) {
val layout = StaticLayoutCompat.Builder(text, paint, maximumTextWidth)
.setLineSpacing(lineSpacingExtra, lineSpacingMultiplier)
.setIncludePad(includeFontPadding)
.build()
if (layout.lineCount <= readMoreMaxLines) {
this.collapseText = text
} else {
this.collapseText = buildSpannedString {
val countUntilMaxLine = layout.getLineVisibleEnd(readMoreMaxLines - 1)
if (text.length <= countUntilMaxLine) {
append(text)
} else {
val overflowText = buildOverflowText()
val overflowTextWidth = StaticLayoutCompat.Builder(
overflowText,
paint,
maximumTextWidth
)
.build()
.getLineWidth(0).toInt()
val textAppearanceSpan = TextAppearanceSpan(
null,
Typeface.NORMAL,
textSize.toInt(),
readMoreTextColor,
null
)
val clickSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
toggle()
}
override fun updateDrawState(ds: TextPaint) {
ds.isUnderlineText = false
}
}
val spans = listOfNotNull(
textAppearanceSpan,
clickSpan
)
val readMoreTextWithStyle = buildReadMoreText(spans = spans.toTypedArray())
val readMorePaint = TextPaint().apply {
set(paint)
textAppearanceSpan.updateMeasureState(this)
}
val readMoreTextWidth = StaticLayoutCompat.Builder(
readMoreTextWithStyle,
readMorePaint,
maximumTextWidth
)
.build()
.getLineWidth(0).toInt()
val readMoreWidth = overflowTextWidth + readMoreTextWidth
val replaceCount = text
.substringOf(layout, line = readMoreMaxLines)
.calculateReplaceCountToBeSingleLineWith(maximumTextWidth - readMoreWidth)
append(text.subSequence(0, countUntilMaxLine - replaceCount))
append(overflowText)
append(readMoreTextWithStyle)
}
}
}
} else {
this.collapseText = text
}
invalidateText()
}
private fun buildOverflowText(
text: String? = readMoreText
): String {
return buildString {
append(ellipsis)
if (text.isNullOrEmpty().not()) append(nbsp)
}
}
private fun buildReadMoreText(
text: String? = readMoreText,
vararg spans: Any
): CharSequence {
return buildSpannedString {
if (text.isNullOrEmpty().not()) {
inSpans(spans = spans) {
append(text)
}
}
}
}
private fun CharSequence.substringOf(layout: Layout, line: Int): CharSequence {
val lastLineStartIndex = layout.getLineStart(line - 1)
val lastLineEndIndex = layout.getLineEnd(line - 1)
return subSequence(lastLineStartIndex, lastLineEndIndex)
}
private fun CharSequence.calculateReplaceCountToBeSingleLineWith(
maximumTextWidth: Int
): Int {
val currentTextBounds = Rect()
var replacedCount = -1
do {
replacedCount++
val replacedText = substring(0, this.length - replacedCount)
paint.getTextBounds(replacedText, 0, replacedText.length, currentTextBounds)
} while (replacedCount < this.length && currentTextBounds.width() >= maximumTextWidth)
val lastVisibleChar: Char? = this.getOrNull(this.length - replacedCount - 1)
val firstOverflowChar: Char? = this.getOrNull(this.length - replacedCount)
if (lastVisibleChar?.isSurrogate() == true && firstOverflowChar?.isHighSurrogate() == false) {
val subText = substring(0, this.length - replacedCount)
if (subText.isNotEmpty()) {
return length - subText.indexOfLast { it.isHighSurrogate() }
}
}
return replacedCount
}
private fun invalidateText() {
if (expanded) {
super.setText(originalText, bufferType)
super.setMaxLines(NO_LIMIT_LINES)
} else {
super.setText(collapseText, bufferType)
super.setMaxLines(readMoreMaxLines)
}
}
private companion object {
private const val NO_LIMIT_LINES = Integer.MAX_VALUE
}
}
\ No newline at end of file
package org.futo.circles.view.read_more
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import android.text.TextUtils
import androidx.annotation.FloatRange
internal class StaticLayoutCompat {
class Builder(
private val text: CharSequence,
private val start: Int,
private val end: Int,
private val paint: TextPaint,
private val width: Int
) {
constructor(text: CharSequence, paint: TextPaint, width: Int) :
this(text, 0, text.length, paint, width)
private var alignment: Layout.Alignment = Layout.Alignment.ALIGN_NORMAL
private var spacingMult = 1f
private var spacingAdd = 0f
private var includePad = true
private var ellipsizedWidth = width
private var ellipsize: TextUtils.TruncateAt? = null
private var maxLines = Integer.MAX_VALUE
fun setLineSpacing(
spacingAdd: Float,
@FloatRange(from = 0.0) spacingMult: Float
): Builder {
this.spacingAdd = spacingAdd
this.spacingMult = spacingMult
return this
}
fun setIncludePad(includePad: Boolean): Builder {
this.includePad = includePad
return this
}
fun build(): StaticLayout {
return StaticLayout.Builder
.obtain(text, start, end, paint, width)
.setAlignment(alignment)
.setLineSpacing(spacingAdd, spacingMult)
.setIncludePad(includePad)
.setEllipsize(ellipsize)
.setEllipsizedWidth(ellipsizedWidth)
.setMaxLines(maxLines)
.build()
}
}
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.futo.circles.view.PostLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.futo.circles.view.PostLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/lTextPost" android:id="@+id/lTextPost"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <org.futo.circles.view.read_more.ReadMoreTextView
android:id="@+id/tvContent" android:id="@+id/tvContent"
style="@style/postMessage" style="@style/postMessage"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="@dimen/post_text_side_margin" android:layout_marginHorizontal="@dimen/post_text_side_margin"
android:autoLink="web" android:autoLink="web"
android:textIsSelectable="true" /> android:textIsSelectable="true"
app:readMoreMaxLines="7"
app:readMoreText="@string/show_more"
app:readMoreTextColor="@color/blue" />
</org.futo.circles.view.PostLayout> </org.futo.circles.view.PostLayout>
\ No newline at end of file
...@@ -16,4 +16,11 @@ ...@@ -16,4 +16,11 @@
<declare-styleable name="GroupPostHeaderView"> <declare-styleable name="GroupPostHeaderView">
<attr name="optionsVisible" format="boolean" /> <attr name="optionsVisible" format="boolean" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ReadMoreTextView">
<attr name="readMoreMaxLines" format="integer" />
<attr name="readMoreText" format="string" />
<attr name="readMoreTextColor" format="reference|color" />
</declare-styleable>
<attr name="readMoreTextViewStyle" format="reference" />
</resources> </resources>
\ No newline at end of file
...@@ -297,6 +297,7 @@ ...@@ -297,6 +297,7 @@
<string name="strong_password">Strong password</string> <string name="strong_password">Strong password</string>
<string name="very_strong_password">Very strong password</string> <string name="very_strong_password">Very strong password</string>
<string name="back">Back</string> <string name="back">Back</string>
<string name="show_more">Show more</string>
<string-array name="report_categories"> <string-array name="report_categories">
<item>@string/crude_language</item> <item>@string/crude_language</item>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment