Код: Выделить всё
package com.example.dynamiccelltable
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.example.dynamiccelltable.ui.theme.DynamicCellTableTheme
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextMeasurer
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
DynamicCellTableTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Timed("Greeting composable") {
Greeting(
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
}
@Composable
fun Greeting(modifier: Modifier = Modifier) {
DynamicCellTable(
rowData = generateMockRowsFor5GSA(),
linkedControllers = listOf(
),
onMaximizeToggle = { _ -> },
modifier = modifier.fillMaxWidth().padding(top=2.dp,bottom=2.dp,start=4.dp,end=4.dp)
)
}
fun generateMockRowsFor5GSA(): List = List(80) { i ->
listOf(
"12:${(10 + i) % 60}:10",
"5G SA",
"31${i % 100}",
"NCI${100000 + i}",
"${620000 + (i % 3000)}",
"-${65 + (i % 15)} dBm",
"-${8 + (i % 10)} dB",
if (i % 6 == 0) "2 sec" else "6 sec"
)
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
DynamicCellTableTheme {
Greeting()
}
}
fun measureColumnWidths(
columnCount: Int,
headers: List,
data: List,
fontFamily: FontFamily,
fontSize: Int,
headerFontSize: Int,
textMeasurer: TextMeasurer,
density: Density
): List {
val space = '\u0020'
val headerWidths = List(columnCount) { col ->
val text = headers.getOrNull(col).orEmpty()
val width = textMeasurer.measure(
text = AnnotatedString("$space$space$space$text$space$space$space"),
style = TextStyle(fontFamily = fontFamily, fontSize = headerFontSize.sp)
).size.width.toFloat()
with(density) { (width + 32).toDp() } // increased padding for headers
}
val dataWidths = List(columnCount) { col ->
val maxWidth = data.maxOfOrNull { row ->
val text = row.getOrNull(col).orEmpty()
textMeasurer.measure(
text = AnnotatedString("$space$space$space$text$space$space$space"),
style = TextStyle(fontFamily = fontFamily, fontSize = fontSize.sp)
).size.width.toFloat()
} ?: 0f
with(density) { (maxWidth + 32).toDp() }
}
return List(columnCount) { col ->
maxOf(headerWidths[col], dataWidths[col])
}
}
class SharedExpansionController {
val isExpandedMutable = mutableStateOf(true)
val isExpanded: State get() = isExpandedMutable
}
val fontPoppins = FontFamily.SansSerif
val lightRed = Color(0xFFFFEBEE)
val borderRed = Color(0xFFFFCDD2)
val titleColor = Color(0xFF4F0C0C)
@SuppressLint("UnusedBoxWithConstraintsScope")
@Composable fun DynamicCellTable(
rowData: List,
modifier: Modifier = Modifier,
linkedControllers: List = emptyList(),
onMaximizeToggle: ((Boolean) -> Unit)? = null
)
{
val highlightedRowIndex = remember { mutableStateOf(null) }
val highlightedColumnIndex = remember { mutableStateOf(null) }
val coroutineScope = rememberCoroutineScope()
var userHasToggled by remember { mutableStateOf(false) }
var toggleExpandNext by remember { mutableStateOf(false) } // initially collapse on first click
val allCollapsed by remember {
derivedStateOf {
linkedControllers.all { !it.isExpanded.value }
}
}
// Trigger recomposition after toggle state changes
LaunchedEffect(userHasToggled, toggleExpandNext) {
snapshotFlow { linkedControllers.map { it.isExpanded.value } }
.collect { /* triggers recomposition */ }
}
val iconRes = if (allCollapsed) {
//R.drawable.minimize // All collapsed ➝ show minimize (clicking it will expand)
R.drawable.ic_launcher_background // All collapsed ➝ show minimize (clicking it will expand)
} else {
//R.drawable.maximize // At least one expanded ➝ show maximize (clicking it will collapse all)
R.drawable.ic_launcher_background // At least one expanded ➝ show maximize (clicking it will collapse all)
}
val columnTitles =
listOf("TIME", "TYPE", "LAC/TAC", "CELL ID", "ARFCN", "LEVEL", "QUALITY", "RUN ")
val listState = rememberLazyListState()
val cornerShape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp)
val textMeasurer = rememberTextMeasurer()
val density = LocalDensity.current
val columnWidths = remember(rowData) {
measureColumnWidths(
columnCount = columnTitles.size,
headers = columnTitles,
data = rowData,
fontFamily = fontPoppins,
fontSize = 9,
headerFontSize = 11,
textMeasurer = textMeasurer,
density = density
)
}
val thumbHeightRatio = remember { mutableFloatStateOf(1f) }
val scrollProgress = remember { mutableFloatStateOf(0f) }
val showScrollbar = remember { mutableStateOf(false) }
LaunchedEffect(listState) {
snapshotFlow { listState.layoutInfo }
.map { layoutInfo ->
val totalItems = layoutInfo.totalItemsCount
val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
val firstIndex = listState.firstVisibleItemIndex
val firstOffset = listState.firstVisibleItemScrollOffset
val estimatedItemHeight = if (layoutInfo.visibleItemsInfo.size >= 2) {
val first = layoutInfo.visibleItemsInfo[0]
val second = layoutInfo.visibleItemsInfo[1]
(second.offset - first.offset).toFloat()
} else 48f
val totalHeight = totalItems * estimatedItemHeight
val currentScrollY = firstIndex * estimatedItemHeight + firstOffset
val ratio = (viewportHeight / totalHeight).coerceIn(0f, 1f)
val progress = (currentScrollY / (totalHeight - viewportHeight)).coerceIn(0f, 1f)
val shouldShow = totalHeight > viewportHeight
Triple(ratio, progress, shouldShow)
}
.distinctUntilChanged()
.collectLatest { (ratio, progress, shouldShow) ->
thumbHeightRatio.floatValue = ratio
scrollProgress.floatValue = progress
showScrollbar.value = shouldShow
}
}
BoxWithConstraints(
modifier = modifier
.clip(cornerShape)
.border(1.dp, borderRed, cornerShape)
.background(lightRed)
) {
val totalContentWidth = columnWidths.reduce { acc, dp -> acc + dp }
val padding = 1.dp * (columnWidths.size + 1)
// ✅ USE maxWidth here directly (BoxWithConstraints scope is being used properly)
val stretchRatio = with(density) {
val availableSpace = maxWidth - padding
val totalContentPx = totalContentWidth.toPx()
if (totalContentPx > 0f) {
(availableSpace.toPx() / totalContentPx).coerceAtMost(1f)
} else 1f
}
val stretchedWidths = columnWidths.mapIndexed { i, col ->
if (i == columnWidths.lastIndex) col else col * stretchRatio
}
// ⬇ The rest remains unchanged
Column(modifier = Modifier.fillMaxSize()) {
// Header
// Header with maximize icon at top right
Box(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.wrapContentWidth()) {
columnTitles.forEachIndexed { index, title ->
Box(
modifier = Modifier
.border(
width = if (highlightedColumnIndex.value == index) 2.dp else 1.dp,
color = if (highlightedColumnIndex.value == index) Color.Blue else borderRed,
)
.background(
if (highlightedColumnIndex.value == index) Color.Yellow else borderRed,
shape = RoundedCornerShape(4.dp)
)
.width(stretchedWidths[index])
.clickable {
highlightedColumnIndex.value = index
coroutineScope.launch {
delay(20000)
if (highlightedColumnIndex.value == index) {
highlightedColumnIndex.value = null
}
}
}
.padding(vertical = 2.dp),
contentAlignment = Alignment.Center
) {
Text(
text = title,
fontWeight = if (highlightedColumnIndex.value == index) FontWeight.Bold else FontWeight.Bold,
fontSize = 11.sp,
color = if (highlightedColumnIndex.value == index) Color.Blue else titleColor,
maxLines = 1
)
}
}
}
// Maximize icon at top-right
androidx.compose.foundation.Image(
painter = painterResource(id = iconRes),
contentDescription = "Toggle Size",
modifier = Modifier
.size(20.dp)
.align(Alignment.TopEnd)
.padding(4.dp)
.clickable {
val shouldExpand = allCollapsed // all collapsed ➝ user intends to expand
onMaximizeToggle?.invoke(shouldExpand)
toggleExpandNext = shouldExpand
userHasToggled = true
}
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xFF2BB28E))
)
// Content + Scrollbar
Box(modifier = Modifier.weight(1f)) {
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxSize()
.background(Color.White)
) {
itemsIndexed(rowData) { index, row ->
val isHighlighted = index == highlightedRowIndex.value
val backgroundColor = when {
isHighlighted -> Color.Yellow
index % 2 == 0 -> Color.White
else -> lightRed.copy(alpha = 0.3f)
}
/*val textColor = when {
isHighlighted -> Color.Blue
highlightedColumnIndex.value != null -> Color.Black
else -> Color.Black
}
*/
Row(
modifier = Modifier
.background(backgroundColor)
.wrapContentWidth()
.clickable {
highlightedRowIndex.value = index
coroutineScope.launch {
delay(6000)
if (highlightedRowIndex.value == index) {
highlightedRowIndex.value = null
}
}
}
) {
for (colIndex in columnTitles.indices) {
val cellText = row.getOrNull(colIndex) ?: ""
Box(
modifier = Modifier
.width(stretchedWidths[colIndex])
.background(
if (highlightedColumnIndex.value == colIndex) Color.Yellow else Color.Transparent
)
.drawWithContent {
drawContent()
val strokeWidth = 1.dp.toPx()
drawLine(
color = borderRed,
start = Offset(0f, size.height - strokeWidth / 2),
end = Offset(size.width, size.height - strokeWidth / 2),
strokeWidth = strokeWidth
)
drawLine(
color = borderRed,
start = Offset(size.width - strokeWidth / 2, 0f),
end = Offset(size.width - strokeWidth / 2, size.height),
strokeWidth = strokeWidth
)
},
contentAlignment = Alignment.Center
) {
val isColumnHighlighted = highlightedColumnIndex.value == colIndex
Text(
text = cellText,
fontFamily = fontPoppins,
fontSize = 9.sp,
color = when {
isHighlighted -> Color.Blue
isColumnHighlighted -> Color.Blue
else -> Color.Black
},
fontWeight = when {
isHighlighted -> FontWeight.Bold
isColumnHighlighted -> FontWeight.Bold
else -> FontWeight.Normal
},
maxLines = 1,
)
}
}
}
}
}
if (showScrollbar.value) {
Canvas(
modifier = Modifier
.align(Alignment.CenterEnd)
.fillMaxHeight()
.width(6.dp)
.padding(end = 2.dp, top = 1.dp, bottom = 4.dp)
) {
val availableHeight = size.height
val thumbHeight = availableHeight * thumbHeightRatio.floatValue
val scrollOffset = scrollProgress.floatValue * (availableHeight - thumbHeight)
drawRoundRect(
color = Color(0xFF4D4B4B).copy(alpha = 0.5f),
topLeft = Offset(0f, scrollOffset),
size = Size(size.width, thumbHeight),
cornerRadius = CornerRadius(3.dp.toPx())
)
}
}
}
Box(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(Color(0xFF2BB28E))
)
// Footer with export icons
// Footer with export icons (minimal height)
Box(
modifier = Modifier
.fillMaxWidth()
.background(borderRed)
.height(18.dp)
.padding(vertical=2.dp)
) {
Row(
modifier = Modifier.align(Alignment.Center),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
listOf(
/*R.drawable.ms_word,
R.drawable.ms_excel,
R.drawable.kml,
R.drawable.pdf
*/
R.drawable.ic_launcher_background,
R.drawable.ic_launcher_background,
R.drawable.ic_launcher_background,
R.drawable.ic_launcher_background
).forEach { icon ->
Icon(
painter = painterResource(id = icon),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier
.padding(horizontal = 6.dp)
.clickable {
// Handle export click
}
)
}
}
}
}
}
}
@Composable
fun Timed(name: String, content: @Composable () -> Unit) {
val start = System.currentTimeMillis()
content()
LaunchedEffect(Unit) {
Log.d("⏱️ Timer", "$name rendered in ${System.currentTimeMillis() - start} ms")
}
}
/*
class TableController(initialData: List) {
private val _rows = mutableStateListOf().apply {
initialData.forEach { add(it.toMutableList()) }
}
val rows: List get() = _rows
fun insertRow(row: List) {
if (row.size == 8) _rows.add(row.toMutableList())
}
fun updateRowCell(rowIndex: Int, colIndex: Int, value: String) {
if (rowIndex in _rows.indices && colIndex in 0..7) {
_rows[rowIndex][colIndex] = value
}
}
}
*/
< /code>
Проблема заключается в том, что если таблица заняла полную высоту экрана, требуется ~ 6s для составления и ~ 4s для каждого навигающего. < /p>
Это драматическое время.--------- beginning of main
--------- beginning of system
---------------------------- PROCESS STARTED (12912) for package com.example.dynamiccelltable ----------------------------
2025-07-21 09:15:44.318 12912-12912 ⏱️ Timer com.example.dynamiccelltable D Greeting composable rendered in 23142 ms
---------------------------- PROCESS ENDED (12912) for package com.example.dynamiccelltable ----------------------------
---------------------------- PROCESS STARTED (13189) for package com.example.dynamiccelltable ----------------------------
2025-07-21 09:16:18.859 13189-13189 ⏱️ Timer com.example.dynamiccelltable D Greeting composable rendered in 6565 ms
Что идет не так с динамическим компостируемым композицией? /> Вот скриншот композиции для полной высоты страницы:
Подробнее здесь: https://stackoverflow.com/questions/797 ... ition-time
Мобильная версия