Remove ReplacementSpan, display diffs using CharacterStyle (#3431)
Remove the use of ReplacementSpan. It turns out this span type is incompatible with spans that occupy more than one line, and the result is that a longer diff can run off the end of the screen. The alternative means that the diff'd text doesn't have additional padding and rounded corners, but it's better than not being visible. Display the most recent version of the status with larger text. Again, consistent with the thread view. Display the avatar, name, and username of the poster in a pinned header at the top of the screen, instead of duplicating the information on every edit. This reduces the amount of redundant information on the screen.
This commit is contained in:
parent
84486c7f13
commit
66a394245b
33 changed files with 266 additions and 223 deletions
|
|
@ -1,8 +1,6 @@
|
|||
package com.keylesspalace.tusky.components.viewthread.edits
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface.DEFAULT_BOLD
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
|
|
@ -11,7 +9,9 @@ import android.text.Html
|
|||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.ReplacementSpan
|
||||
import android.text.TextPaint
|
||||
import android.text.style.CharacterStyle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -33,11 +33,9 @@ import com.keylesspalace.tusky.util.aspectRatios
|
|||
import com.keylesspalace.tusky.util.decodeBlurHash
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||
import com.keylesspalace.tusky.util.setClickableText
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.keylesspalace.tusky.viewdata.toViewData
|
||||
import org.xml.sax.XMLReader
|
||||
|
|
@ -52,13 +50,28 @@ class ViewEditsAdapter(
|
|||
|
||||
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||
|
||||
/** Size of large text in this theme, in px */
|
||||
var largeTextSizePx: Float = 0f
|
||||
|
||||
/** Size of medium text in this theme, in px */
|
||||
var mediumTextSizePx: Float = 0f
|
||||
|
||||
override fun onCreateViewHolder(
|
||||
parent: ViewGroup,
|
||||
viewType: Int
|
||||
): BindingHolder<ItemStatusEditBinding> {
|
||||
val binding = ItemStatusEditBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
||||
binding.statusEditMediaPreview.clipToOutline = true
|
||||
|
||||
val typedValue = TypedValue()
|
||||
val context = binding.root.context
|
||||
val displayMetrics = context.resources.displayMetrics
|
||||
context.theme.resolveAttribute(R.attr.status_text_large, typedValue, true)
|
||||
largeTextSizePx = typedValue.getDimension(displayMetrics)
|
||||
context.theme.resolveAttribute(R.attr.status_text_medium, typedValue, true)
|
||||
mediumTextSizePx = typedValue.getDimension(displayMetrics)
|
||||
|
||||
return BindingHolder(binding)
|
||||
}
|
||||
|
||||
|
|
@ -69,24 +82,26 @@ class ViewEditsAdapter(
|
|||
|
||||
val context = binding.root.context
|
||||
|
||||
val avatarRadius: Int = context.resources
|
||||
.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
|
||||
loadAvatar(edit.account.avatar, binding.statusEditAvatar, avatarRadius, animateAvatars)
|
||||
|
||||
val infoStringRes = if (position == edits.size - 1) {
|
||||
val infoStringRes = if (position == edits.lastIndex) {
|
||||
R.string.status_created_info
|
||||
} else {
|
||||
R.string.status_edit_info
|
||||
}
|
||||
|
||||
// Show the most recent version of the status using large text to make it clearer for
|
||||
// the user, and for similarity with thread view.
|
||||
val variableTextSize = if (position == edits.lastIndex) {
|
||||
mediumTextSizePx
|
||||
} else {
|
||||
largeTextSizePx
|
||||
}
|
||||
binding.statusEditContentWarningDescription.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize)
|
||||
binding.statusEditContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize)
|
||||
binding.statusEditMediaSensitivity.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize)
|
||||
|
||||
val timestamp = absoluteTimeFormatter.format(edit.createdAt, false)
|
||||
|
||||
binding.statusEditInfo.text = context.getString(
|
||||
infoStringRes,
|
||||
edit.account.name.unicodeWrap(),
|
||||
timestamp
|
||||
).emojify(edit.account.emojis, binding.statusEditInfo, animateEmojis)
|
||||
binding.statusEditInfo.text = context.getString(infoStringRes, timestamp)
|
||||
|
||||
if (edit.spoilerText.isEmpty()) {
|
||||
binding.statusEditContentWarningDescription.hide()
|
||||
|
|
@ -198,6 +213,11 @@ class ViewEditsAdapter(
|
|||
}
|
||||
|
||||
override fun getItemCount() = edits.size
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_EDITS_NEWEST = 0
|
||||
private const val VIEW_TYPE_EDITS = 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -266,98 +286,31 @@ class TuskyTagHandler(val context: Context) : Html.TagHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A span that draws text with additional padding at the start/end of the text. The padding
|
||||
* is the width of [separator].
|
||||
*
|
||||
* Note: The separator string is not included in the final text, so it will not be included
|
||||
* if the user cuts or copies the text.
|
||||
*/
|
||||
open class LRPaddedSpan(val separator: String = " ") : ReplacementSpan() {
|
||||
/** The width of the separator string, used as padding */
|
||||
var paddingWidth = 0f
|
||||
|
||||
/** Measured width of the span */
|
||||
var spanWidth = 0f
|
||||
|
||||
override fun getSize(
|
||||
paint: Paint,
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
fm: Paint.FontMetricsInt?
|
||||
): Int {
|
||||
paddingWidth = paint.measureText(separator, 0, separator.length)
|
||||
spanWidth = (paddingWidth * 2) + paint.measureText(text, start, end)
|
||||
return spanWidth.toInt()
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas,
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
x: Float,
|
||||
top: Int,
|
||||
y: Int,
|
||||
bottom: Int,
|
||||
paint: Paint
|
||||
) {
|
||||
canvas.drawText(text?.subSequence(start, end).toString(), x + paddingWidth, y.toFloat(), paint)
|
||||
}
|
||||
}
|
||||
|
||||
/** Span that signifies deleted text */
|
||||
class DeletedTextSpan(context: Context) : LRPaddedSpan() {
|
||||
private val bgPaint = Paint()
|
||||
val radius: Float
|
||||
class DeletedTextSpan(context: Context) : CharacterStyle() {
|
||||
private var bgColor: Int
|
||||
|
||||
init {
|
||||
bgPaint.color = context.getColor(R.color.view_edits_background_delete)
|
||||
radius = context.resources.getDimension(R.dimen.lrPaddedSpanRadius)
|
||||
bgColor = context.getColor(R.color.view_edits_background_delete)
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas,
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
x: Float,
|
||||
top: Int,
|
||||
y: Int,
|
||||
bottom: Int,
|
||||
paint: Paint
|
||||
) {
|
||||
canvas.drawRoundRect(x, top.toFloat(), x + spanWidth, bottom.toFloat(), radius, radius, bgPaint)
|
||||
paint.isStrikeThruText = true
|
||||
super.draw(canvas, text, start, end, x, top, y, bottom, paint)
|
||||
override fun updateDrawState(tp: TextPaint) {
|
||||
tp.bgColor = bgColor
|
||||
tp.isStrikeThruText = true
|
||||
}
|
||||
}
|
||||
|
||||
/** Span that signifies inserted text */
|
||||
class InsertedTextSpan(context: Context) : LRPaddedSpan() {
|
||||
val bgPaint = Paint()
|
||||
val radius: Float
|
||||
class InsertedTextSpan(context: Context) : CharacterStyle() {
|
||||
private var bgColor: Int
|
||||
|
||||
init {
|
||||
bgPaint.color = context.getColor(R.color.view_edits_background_insert)
|
||||
radius = context.resources.getDimension(R.dimen.lrPaddedSpanRadius)
|
||||
bgColor = context.getColor(R.color.view_edits_background_insert)
|
||||
}
|
||||
|
||||
override fun draw(
|
||||
canvas: Canvas,
|
||||
text: CharSequence?,
|
||||
start: Int,
|
||||
end: Int,
|
||||
x: Float,
|
||||
top: Int,
|
||||
y: Int,
|
||||
bottom: Int,
|
||||
paint: Paint
|
||||
) {
|
||||
canvas.drawRoundRect(x, top.toFloat(), x + spanWidth, bottom.toFloat(), radius, radius, bgPaint)
|
||||
paint.typeface = DEFAULT_BOLD
|
||||
super.draw(canvas, text, start, end, x, top, y, bottom, paint)
|
||||
override fun updateDrawState(tp: TextPaint) {
|
||||
tp.bgColor = bgColor
|
||||
tp.typeface = DEFAULT_BOLD
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,13 +37,16 @@ import com.keylesspalace.tusky.BottomSheetActivity
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.StatusListActivity
|
||||
import com.keylesspalace.tusky.components.account.AccountActivity
|
||||
import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding
|
||||
import com.keylesspalace.tusky.databinding.FragmentViewEditsBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.emojify
|
||||
import com.keylesspalace.tusky.util.hide
|
||||
import com.keylesspalace.tusky.util.loadAvatar
|
||||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.util.unicodeWrap
|
||||
import com.keylesspalace.tusky.util.viewBinding
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
|
|
@ -54,7 +57,7 @@ import java.io.IOException
|
|||
import javax.inject.Inject
|
||||
|
||||
class ViewEditsFragment :
|
||||
Fragment(R.layout.fragment_view_thread),
|
||||
Fragment(R.layout.fragment_view_edits),
|
||||
LinkListener,
|
||||
OnRefreshListener,
|
||||
MenuProvider,
|
||||
|
|
@ -65,7 +68,7 @@ class ViewEditsFragment :
|
|||
|
||||
private val viewModel: ViewEditsViewModel by viewModels { viewModelFactory }
|
||||
|
||||
private val binding by viewBinding(FragmentViewThreadBinding::bind)
|
||||
private val binding by viewBinding(FragmentViewEditsBinding::bind)
|
||||
|
||||
private lateinit var statusId: String
|
||||
|
||||
|
|
@ -88,6 +91,7 @@ class ViewEditsFragment :
|
|||
val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false)
|
||||
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||
val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true)
|
||||
val avatarRadius: Int = requireContext().resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewModel.uiState.collect { uiState ->
|
||||
|
|
@ -130,6 +134,15 @@ class ViewEditsFragment :
|
|||
useBlurhash = useBlurhash,
|
||||
listener = this@ViewEditsFragment
|
||||
)
|
||||
|
||||
// Focus on the most recent version
|
||||
(binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPosition(0)
|
||||
|
||||
val account = uiState.edits.first().account
|
||||
loadAvatar(account.avatar, binding.statusAvatar, avatarRadius, animateAvatars)
|
||||
|
||||
binding.statusDisplayName.text = account.name.unicodeWrap().emojify(account.emojis, binding.statusDisplayName, animateEmojis)
|
||||
binding.statusUsername.text = account.username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,10 +98,7 @@ class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : Vie
|
|||
if (i < sortedEdits.size - 1) {
|
||||
currentContent = previousContent
|
||||
previousContent = loader.load(
|
||||
sortedEdits[i + 1].content.replace(
|
||||
"<br>",
|
||||
"<br/>"
|
||||
)
|
||||
sortedEdits[i + 1].content.replace("<br>", "<br/>")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue