Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Expand Up @@ -245,6 +245,12 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
init {
OneKeyImageGlideRegistry.ensureRegistered(context)
hostView.updateSkeletonStyle()
installHostCallbacks()
applyContentFit()
applyVariant()
}

private fun installHostCallbacks() {
hostView.onReadyForRequest = { scheduleLoad() }
hostView.onAttachmentChanged = { attached ->
if (attached) {
Expand All @@ -255,8 +261,6 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
displayRunnable = null
}
}
applyContentFit()
applyVariant()
}

override fun afterUpdate() {
Expand All @@ -276,21 +280,38 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :

override fun prepareForRecycle() {
resetForReuse()
disposed = false
installHostCallbacks()
}

override fun onDropView() {
resetForReuse()
disposed = true
hostView.onReadyForRequest = null
hostView.onAttachmentChanged = null
cancelPendingCallbacks()
clearEventCallbacks()
hostView.autoplayEnabled = false
hostView.skeletonRequested = false
hostView.syncPlayback()
if (!usesApplicationRequestManager && hostView.isAttachedToWindow && hostView.drawable != null) {
// ScreenStack can still draw a dropped view during its removal transition.
// Keep the completed Glide target too: clearing it may recycle the bitmap.
hostView.onAttachmentChanged = { attached -> if (!attached) finishDrop() }
} else {
finishDrop()
}
super.onDropView()
}

override fun dispose() {
disposed = true
cancelCurrent(invalidateGeneration = true)
private fun finishDrop() {
resetForReuse()
hostView.onReadyForRequest = null
hostView.onAttachmentChanged = null
}

override fun dispose() {
disposed = true
clearEventCallbacks()
finishDrop()
super.dispose()
}

Expand All @@ -317,11 +338,7 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
resizeWidth = null
overscan = 1.1
loadingStrategy = OneKeyImageLoadingStrategy.STATIC
onLoadStart = null
onLoad = null
onDisplay = null
onError = null
onLoadEnd = null
clearEventCallbacks()
suppressPropEffects = false
requestActive = false
displayState = DisplayState.LOADING
Expand Down Expand Up @@ -553,17 +570,29 @@ class HybridOneKeyImage(private val context: ThemedReactContext) :
}

private fun cancelCurrent(invalidateGeneration: Boolean) {
cancelPendingCallbacks()
clearCurrentTarget()
requestActive = false
hostView.skeletonRequested = false
if (invalidateGeneration) generation++
}

private fun cancelPendingCallbacks() {
loadRunnable?.let(hostView::removeCallbacks)
loadRunnable = null
displayRunnable?.let(hostView::removeCallbacks)
displayRunnable = null
pendingDisplayGeneration = null
fallbackRunnable?.let(hostView::removeCallbacks)
fallbackRunnable = null
clearCurrentTarget()
requestActive = false
hostView.skeletonRequested = false
if (invalidateGeneration) generation++
}

private fun clearEventCallbacks() {
onLoadStart = null
onLoad = null
onDisplay = null
onError = null
onLoadEnd = null
}

private fun clearCurrentTarget() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.margelo.nitro.nativelist

import org.json.JSONObject

/** Only text and tone inside existing subtitle slots may change without rebuilding an identity row. */
internal fun canUpdateIdentitySubtitle(previous: NativeListItem, next: NativeListItem): Boolean {
if (previous.key != next.key || previous.type != "identity" || next.type != "identity") return false

fun structure(item: NativeListItem): String? {
val data = JSONObject(item.content)
if (data.optString("presentation") != "accountSelector") return null
val segments = data.optJSONArray("subtitleSegments") ?: return null
if (segments.length() == 0) return null
for (index in 0 until segments.length()) {
val segment = segments.optJSONObject(index) ?: return null
segment.remove("text")
segment.remove("textSegments")
segment.remove("tone")
}
data.remove("accessibilityLabel")
return data.toString()
}

val previousStructure = structure(previous) ?: return false
return previousStructure == structure(next)
}
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,9 @@ internal class NativeListRowView(
private val selectorLineHeights = mutableMapOf<TextView, Int>()
private val selectorFontSizes = mutableMapOf<TextView, Float>()
private val selectorImages = mutableListOf<OneKeyImageReusableView>()
private val identitySubtitleLabels = mutableListOf<TextView>()
private var boundTheme: String? = null
private var boundOrientation: String? = null
private var selectorHeight: Int? = null
private val leadingImages = List(3) { OneKeyImageReusableView(reactContext) }
private val leadingOverlayBackground = View(context)
Expand Down Expand Up @@ -649,14 +652,34 @@ internal class NativeListRowView(
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String,
useSourceScale: Boolean = false,
) {
val previous = tag as? NativeListItem
val sourceScale = item.usesSelectorSourceScale || useSourceScale
if (
boundKey == item.key && previous != null &&
boundTheme == theme?.toString() && currentLayout == layout &&
boundOrientation == listOrientation && selectorUsesSourceScale == sourceScale &&
identitySubtitleLabels.isNotEmpty() && canUpdateIdentitySubtitle(previous, item)
) {
// Balance patches keep the same avatar and its in-flight/completed image request.
val segments = item.json.getJSONArray("subtitleSegments")
identitySubtitleLabels.forEachIndexed { index, label ->
bindIdentitySubtitleLabel(label, segments.getJSONObject(index), theme)
}
contentDescription = item.json.optString("accessibilityLabel", item.json.optString("title"))
bindSelection(item, theme, layout, itemIndex, selected, checkboxState)
applySelectorTypography(item)
return
}
val shouldRestorePressed = touchPressed && boundKey == item.key
invalidateCurrentBinding()
bindingEpoch += 1
boundKey = item.key
touchPressed = shouldRestorePressed
currentLayout = layout
tag = item
selectorUsesSourceScale = item.usesSelectorSourceScale || useSourceScale
selectorUsesSourceScale = sourceScale
boundTheme = theme?.toString()
boundOrientation = listOrientation
reorderActive = false
leadingImages.forEach(OneKeyImageReusableView::prepareForReuse)
secondaryImage.prepareForReuse()
Expand Down Expand Up @@ -939,6 +962,7 @@ internal class NativeListRowView(
}

private fun resetViews() {
identitySubtitleLabels.clear()
clipChildren = true
clipToPadding = true
selectorOriginalFontFeatures.forEach { (view, original) -> view.fontFeatureSettings = original }
Expand Down Expand Up @@ -1442,17 +1466,15 @@ internal class NativeListRowView(
line.addView(dot, LayoutParams(dp(4), dp(4)).apply { marginStart = dp(6); marginEnd = dp(6) })
}
val label = TextView(context).apply {
text = segment.optString("text")
typeface = NativeListFonts.regular(context)
textSize = sp(14f)
includeFontPadding = false
maxLines = 1
ellipsize = TextUtils.TruncateAt.END
val toneKey = when (segment.optString("tone")) { "primary" -> "primaryText"; "disabled" -> "disabledText"; "caution" -> "caution"; "positive" -> "positive"; "negative" -> "negative"; else -> "secondaryText" }
setTextColor(color(theme, toneKey, if (toneKey == "caution") "#AB6400" else "#0000009B"))
}
TextViewCompat.setLineHeight(label, dp(20))
applyValueSegments(label, segment.optJSONArray("textSegments"), 14, 20, false)
bindIdentitySubtitleLabel(label, segment, theme)
identitySubtitleLabels.add(label)
line.addView(label, LayoutParams(LayoutParams.WRAP_CONTENT, dp(20)))
}
mainColumn.addView(line, 2, LayoutParams(LayoutParams.MATCH_PARENT, dp(20)))
Expand Down Expand Up @@ -2311,6 +2333,20 @@ internal class NativeListRowView(
applyValueSegments(trailingViews[0], item.json.optJSONArray("valueSegments"))
}

private fun bindIdentitySubtitleLabel(label: TextView, segment: JSONObject, theme: JSONObject?) {
label.text = segment.optString("text")
val toneKey = when (segment.optString("tone")) {
"primary" -> "primaryText"
"disabled" -> "disabledText"
"caution" -> "caution"
"positive" -> "positive"
"negative" -> "negative"
else -> "secondaryText"
}
label.setTextColor(color(theme, toneKey, if (toneKey == "caution") "#AB6400" else "#0000009B"))
applyValueSegments(label, segment.optJSONArray("textSegments"), 14, 20, false)
}

// OneKey patch: preserve compact zero-count digits without changing their baseline.
private fun applyValueSegments(view: TextView, segments: JSONArray?, fontSize: Int = 16, lineHeight: Int = 24, medium: Boolean = true) {
if (segments == null || segments.length() == 0) return
Expand Down