Keep open positions carousel stable

This commit is contained in:
Codex
2026-07-09 22:50:33 +03:00
parent 77a063d263
commit 6fb79ee2a9
2 changed files with 62 additions and 8 deletions
+2 -2
View File
@@ -10,7 +10,7 @@ android {
applicationId = "xyz.kusoft.tradebotmonitor" applicationId = "xyz.kusoft.tradebotmonitor"
minSdk = 26 minSdk = 26
targetSdk = 36 targetSdk = 36
versionCode = 16 versionCode = 17
versionName = "0.2.13" versionName = "0.2.14"
} }
} }
@@ -62,10 +62,12 @@ class MainActivity : Activity() {
var positionsScroller: HorizontalScrollView? = null, var positionsScroller: HorizontalScrollView? = null,
var positionsList: LinearLayout? = null, var positionsList: LinearLayout? = null,
var closedTradesContainer: LinearLayout? = null, var closedTradesContainer: LinearLayout? = null,
val positionOrder: MutableList<String> = mutableListOf(),
val positions: MutableMap<String, PositionBinding> = linkedMapOf(), val positions: MutableMap<String, PositionBinding> = linkedMapOf(),
) )
private data class PositionBinding( private data class PositionBinding(
var index: TextView? = null,
var value: TextView? = null, var value: TextView? = null,
var quantity: TextView? = null, var quantity: TextView? = null,
var entryPrice: TextView? = null, var entryPrice: TextView? = null,
@@ -165,7 +167,7 @@ class MainActivity : Activity() {
trainingStatusSignature = newTrainingStatusSignature trainingStatusSignature = newTrainingStatusSignature
lastError = "" lastError = ""
ensureSelectedSymbol() ensureSelectedSymbol()
if (silent && hadSnapshot && !hadError && updateVisibleScreen(data)) { if (silent && hadSnapshot && updateVisibleScreen(data)) {
return@post return@post
} }
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged)) { if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged)) {
@@ -178,6 +180,9 @@ class MainActivity : Activity() {
val hadError = lastError.isNotBlank() val hadError = lastError.isNotBlank()
lastError = error.message ?: "ошибка подключения" lastError = error.message ?: "ошибка подключения"
if (!silent) toast(lastError) if (!silent) toast(lastError)
if (silent && hadSnapshot) {
return@post
}
if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged = false)) { if (shouldRenderAfterRefresh(silent, hadSnapshot, hadError, trainingChanged = false)) {
render() render()
} }
@@ -258,10 +263,8 @@ class MainActivity : Activity() {
val binding = assetsBindings ?: return false val binding = assetsBindings ?: return false
val positionSignature = positionsSignature(data.positions) val positionSignature = positionsSignature(data.positions)
if (binding.positionSignature != positionSignature) { if (binding.positionSignature != positionSignature) {
binding.positionsContainer?.let { container -> if (!syncPositionsList(binding, data.positions)) return false
rebuildPositionsContent(container, data.positions, binding.positionsScroller?.scrollX ?: 0) binding.positionSignature = positionSignature
binding.positionSignature = positionSignature
} ?: return false
} }
val closedSignature = closedTradesSignature(data.closedTrades, data.closedTradesSummary) val closedSignature = closedTradesSignature(data.closedTrades, data.closedTradesSummary)
if (binding.closedTradesSignature != closedSignature) { if (binding.closedTradesSignature != closedSignature) {
@@ -837,6 +840,7 @@ class MainActivity : Activity() {
private fun rebuildPositionsContent(container: LinearLayout, positions: List<PositionData>, preserveScrollX: Int = 0) { private fun rebuildPositionsContent(container: LinearLayout, positions: List<PositionData>, preserveScrollX: Int = 0) {
container.removeAllViews() container.removeAllViews()
assetsBindings?.positions?.clear() assetsBindings?.positions?.clear()
assetsBindings?.positionOrder?.clear()
assetsBindings?.positionsScroller = null assetsBindings?.positionsScroller = null
assetsBindings?.positionsList = null assetsBindings?.positionsList = null
if (positions.isEmpty()) { if (positions.isEmpty()) {
@@ -868,13 +872,61 @@ class MainActivity : Activity() {
cardWidth: Int = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260)), cardWidth: Int = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260)),
) { ) {
list.removeAllViews() list.removeAllViews()
assetsBindings?.positionOrder?.clear()
positions.forEachIndexed { index, position -> positions.forEachIndexed { index, position ->
assetsBindings?.positionOrder?.add(positionKey(position))
list.addView(positionCard(position, index + 1, positions.size), LinearLayout.LayoutParams(cardWidth, ViewGroup.LayoutParams.WRAP_CONTENT).apply { list.addView(positionCard(position, index + 1, positions.size), LinearLayout.LayoutParams(cardWidth, ViewGroup.LayoutParams.WRAP_CONTENT).apply {
if (index < positions.lastIndex) setMargins(0, 0, dp(12), 0) if (index < positions.lastIndex) setMargins(0, 0, dp(12), 0)
}) })
} }
} }
private fun syncPositionsList(binding: AssetsBindings, positions: List<PositionData>): Boolean {
val container = binding.positionsContainer ?: return false
val list = binding.positionsList
if (list == null || positions.isEmpty() || binding.positionOrder.isEmpty()) {
rebuildPositionsContent(container, positions, binding.positionsScroller?.scrollX ?: 0)
return true
}
val byKey = positions.associateBy(::positionKey)
var index = 0
while (index < binding.positionOrder.size) {
val key = binding.positionOrder[index]
if (!byKey.containsKey(key)) {
binding.positionOrder.removeAt(index)
binding.positions.remove(key)
list.removeViewAt(index)
} else {
index += 1
}
}
val cardWidth = (resources.displayMetrics.widthPixels - dp(64)).coerceAtLeast(dp(260))
positions.forEach { position ->
val key = positionKey(position)
if (!binding.positionOrder.contains(key)) {
binding.positionOrder.add(key)
list.addView(positionCard(position, binding.positionOrder.size, positions.size), LinearLayout.LayoutParams(cardWidth, ViewGroup.LayoutParams.WRAP_CONTENT))
}
}
updatePositionCardChrome(binding)
return true
}
private fun updatePositionCardChrome(binding: AssetsBindings) {
val total = binding.positionOrder.size
binding.positionOrder.forEachIndexed { index, key ->
setText(binding.positions[key]?.index, "${index + 1}/$total")
}
val list = binding.positionsList ?: return
for (i in 0 until list.childCount) {
val params = (list.getChildAt(i).layoutParams as? LinearLayout.LayoutParams) ?: continue
params.setMargins(0, 0, if (i < list.childCount - 1) dp(12) else 0, 0)
list.getChildAt(i).layoutParams = params
}
}
private fun positionCard(position: PositionData, index: Int, total: Int): View = private fun positionCard(position: PositionData, index: Int, total: Int): View =
LinearLayout(this).apply { LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
@@ -884,7 +936,9 @@ class MainActivity : Activity() {
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
addView(text(position.symbol, 20f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)) addView(text(position.symbol, 20f, Typeface.BOLD), LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f))
addView(text("$index/$total", 12f, Typeface.BOLD, palette.muted)) val indexText = text("$index/$total", 12f, Typeface.BOLD, palette.muted)
assetsBindings?.positions?.getOrPut(positionKey(position)) { PositionBinding() }?.index = indexText
addView(indexText)
}) })
val valueText = text("${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", 15f, Typeface.BOLD, colorForSigned(position.unrealizedPnlPercent)) val valueText = text("${money(position.marketValue)} · ${signedPercent(position.unrealizedPnlPercent, 2)}", 15f, Typeface.BOLD, colorForSigned(position.unrealizedPnlPercent))
addView(valueText.top(dp(8))) addView(valueText.top(dp(8)))