diff --git a/app/src/androidTest/kotlin/info/appdev/chartexample/StartTest.kt b/app/src/androidTest/kotlin/info/appdev/chartexample/StartTest.kt index 37a12937f..5bbdbaf80 100644 --- a/app/src/androidTest/kotlin/info/appdev/chartexample/StartTest.kt +++ b/app/src/androidTest/kotlin/info/appdev/chartexample/StartTest.kt @@ -27,6 +27,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import info.appdev.chartexample.compose.HorizontalBarComposeActivity import info.appdev.chartexample.compose.HorizontalBarFullComposeActivity +import info.appdev.chartexample.compose.MultiLineComposeActivity import info.appdev.chartexample.fragments.ViewPagerSimpleChartDemo import info.appdev.chartexample.notimportant.ContentItem import info.appdev.chartexample.notimportant.DemoBase @@ -185,6 +186,16 @@ class StartTest { optionMenu = "$index->$menuTitle" Timber.d("Testing Compose menu item: $optionMenu") + // Check if menu item exists first + try { + composeTestRule + .onNodeWithTag("menuItem_$menuTitle") + .assertExists() + } catch (_: AssertionError) { + Timber.e("Menu item '$menuTitle' not found for ${contentClass.simpleName}, skipping") + return@forEach + } + // Click the menu item composeTestRule .onNodeWithTag("menuItem_$menuTitle") @@ -265,6 +276,7 @@ class StartTest { contentItem.clazz == LineChartTimeActivity::class.java || contentItem.clazz == HorizontalBarComposeActivity::class.java || contentItem.clazz == HorizontalBarFullComposeActivity::class.java || + contentItem.clazz == MultiLineComposeActivity::class.java || contentItem.clazz == GradientActivity::class.java || contentItem.clazz == TimeLineActivity::class.java ) { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2550255e3..648410d1e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ + diff --git a/app/src/main/kotlin/info/appdev/chartexample/compose/HorizontalBarFullComposeActivity.kt b/app/src/main/kotlin/info/appdev/chartexample/compose/HorizontalBarFullComposeActivity.kt index 0414bc142..414c1342a 100644 --- a/app/src/main/kotlin/info/appdev/chartexample/compose/HorizontalBarFullComposeActivity.kt +++ b/app/src/main/kotlin/info/appdev/chartexample/compose/HorizontalBarFullComposeActivity.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -66,6 +67,15 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { onSaveToGallery: () -> Unit, onViewGithub: () -> Unit ) { + // State management + var showValues by remember { mutableStateOf(true) } + var showIcons by remember { mutableStateOf(false) } + var highlightEnabled by remember { mutableStateOf(true) } + var pinchZoomEnabled by remember { mutableStateOf(true) } + var autoScaleMinMaxEnabled by remember { mutableStateOf(false) } + var barBordersEnabled by remember { mutableStateOf(false) } + var animationTrigger by remember { mutableStateOf(0) } + var showMenu by remember { mutableStateOf(false) } var seekBarXValue by remember { mutableFloatStateOf(12f) } var seekBarYValue by remember { mutableFloatStateOf(50f) } @@ -73,7 +83,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { Scaffold( topBar = { TopAppBar( - title = { Text(this.javaClass.simpleName.replace("Activity", "")) }, + title = { Text("HorizontalBarFullCompose") }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primary, titleContentColor = MaterialTheme.colorScheme.onPrimary @@ -107,7 +117,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Toggle Values") }, onClick = { showMenu = false - toggleValues() + showValues = !showValues }, modifier = Modifier.testTag("menuItem_Toggle Values") ) @@ -115,7 +125,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Toggle Icons") }, onClick = { showMenu = false - toggleIcons() + showIcons = !showIcons }, modifier = Modifier.testTag("menuItem_Toggle Icons") ) @@ -123,7 +133,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Toggle Highlight") }, onClick = { showMenu = false - toggleHighlight() + highlightEnabled = !highlightEnabled }, modifier = Modifier.testTag("menuItem_Toggle Highlight") ) @@ -131,7 +141,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Toggle Pinch Zoom") }, onClick = { showMenu = false - togglePinchZoom() + pinchZoomEnabled = !pinchZoomEnabled }, modifier = Modifier.testTag("menuItem_Toggle Pinch Zoom") ) @@ -139,7 +149,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Toggle Auto Scale MinMax") }, onClick = { showMenu = false - toggleAutoScaleMinMax() + autoScaleMinMaxEnabled = !autoScaleMinMaxEnabled }, modifier = Modifier.testTag("menuItem_Toggle Auto Scale MinMax") ) @@ -147,7 +157,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Toggle Bar Borders") }, onClick = { showMenu = false - toggleBarBorders() + barBordersEnabled = !barBordersEnabled }, modifier = Modifier.testTag("menuItem_Toggle Bar Borders") ) @@ -155,7 +165,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Animate X") }, onClick = { showMenu = false - animateX() + animationTrigger++ }, modifier = Modifier.testTag("menuItem_Animate X") ) @@ -163,7 +173,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Animate Y") }, onClick = { showMenu = false - animateY() + animationTrigger++ }, modifier = Modifier.testTag("menuItem_Animate Y") ) @@ -171,7 +181,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { text = { Text("Animate XY") }, onClick = { showMenu = false - animateXY() + animationTrigger++ }, modifier = Modifier.testTag("menuItem_Animate XY") ) @@ -196,51 +206,74 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { .background(Color.White) ) { // Chart - Using Compose HorizontalBarChart - val barData = remember(seekBarXValue, seekBarYValue) { - createBarData(seekBarXValue.toInt(), seekBarYValue) + val barData = remember( + seekBarXValue, + seekBarYValue, + showIcons, + barBordersEnabled, + showValues + ) { + createBarData( + seekBarXValue.toInt(), + seekBarYValue, + showIcons, + barBordersEnabled, + showValues + ) } - HorizontalBarChart( - data = barData, - modifier = Modifier - .fillMaxWidth() - .weight(1f), - drawValueAboveBar = true, - drawBarShadow = false, - animationDuration = 2500, - onValueSelected = { entry, highlight -> - entry?.let { - Timber.d("Selected: x=${it.x}, y=${it.y}") + key(showIcons) { + HorizontalBarChart( + data = barData, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + .testTag("horizontalBarChart_$showIcons"), + drawValueAboveBar = true, + drawBarShadow = false, + scaleEnabled = pinchZoomEnabled, + touchEnabled = true, + dragEnabled = true, + highlightFullBarEnabled = highlightEnabled, + animationDuration = if (animationTrigger > 0) 2500 else 0, + onValueSelected = { entry, _ -> + entry?.let { + Timber.d("Selected: x=${it.x}, y=${it.y}") + } + }, + xAxisConfig = { xAxis -> + xAxis.position = XAxisPosition.BOTTOM + xAxis.typeface = tfLight + xAxis.isDrawAxisLine = true + xAxis.isDrawGridLines = false + xAxis.granularity = 10f + }, + leftAxisConfig = { leftAxis -> + leftAxis.typeface = tfLight + leftAxis.isDrawAxisLine = true + leftAxis.isDrawGridLines = true + if (!autoScaleMinMaxEnabled) { + leftAxis.axisMinimum = 0f + } + }, + rightAxisConfig = { rightAxis -> + rightAxis.typeface = tfLight + rightAxis.isDrawAxisLine = true + rightAxis.isDrawGridLines = false + if (!autoScaleMinMaxEnabled) { + rightAxis.axisMinimum = 0f + } + }, + legend = { legend -> + legend.verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM + legend.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT + legend.orientation = Legend.LegendOrientation.HORIZONTAL + legend.setDrawInside(false) + legend.formSize = 8f + legend.xEntrySpace = 4f } - }, - xAxisConfig = { xAxis -> - xAxis.position = XAxisPosition.BOTTOM - xAxis.typeface = tfLight - xAxis.isDrawAxisLine = true - xAxis.isDrawGridLines = false - xAxis.granularity = 10f - }, - leftAxisConfig = { leftAxis -> - leftAxis.typeface = tfLight - leftAxis.isDrawAxisLine = true - leftAxis.isDrawGridLines = true - leftAxis.axisMinimum = 0f - }, - rightAxisConfig = { rightAxis -> - rightAxis.typeface = tfLight - rightAxis.isDrawAxisLine = true - rightAxis.isDrawGridLines = false - rightAxis.axisMinimum = 0f - }, - legend = { legend -> - legend.verticalAlignment = Legend.LegendVerticalAlignment.BOTTOM - legend.horizontalAlignment = Legend.LegendHorizontalAlignment.LEFT - legend.orientation = Legend.LegendOrientation.HORIZONTAL - legend.setDrawInside(false) - legend.formSize = 8f - legend.xEntrySpace = 4f - } - ) + ) + } // SeekBar X with label Row( @@ -289,7 +322,7 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { } } - private fun createBarData(count: Int, range: Float): BarData { + private fun createBarData(count: Int, range: Float, showIcons: Boolean, barBordersEnabled: Boolean, showValues: Boolean): BarData { val barWidth = 9f val spaceForBar = 10f val values = ArrayList() @@ -297,16 +330,25 @@ class HorizontalBarFullComposeActivity : DemoBaseCompose() { for (i in 0.. Unit, + onViewGithub: () -> Unit + ) { + var showMenu by remember { mutableStateOf(false) } + var seekBarXValue by remember { mutableFloatStateOf(20f) } + var seekBarYValue by remember { mutableFloatStateOf(100f) } + + // State for toggles + var showValues by remember { mutableStateOf(false) } + var pinchZoom by remember { mutableStateOf(false) } + var autoScaleMinMax by remember { mutableStateOf(false) } + var showFilled by remember { mutableStateOf(false) } + var showCircles by remember { mutableStateOf(true) } + var lineMode by remember { mutableStateOf(LineDataSet.Mode.LINEAR) } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("MultiLineChartCompose") }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary + ), + actions = { + Box { + IconButton( + onClick = { showMenu = true }, + modifier = Modifier.testTag("menuButton") + ) { + Icon( + Icons.Default.MoreVert, + contentDescription = "Menu", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + DropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + modifier = Modifier.testTag("dropdownMenu") + ) { + DropdownMenuItem( + text = { Text("View on GitHub") }, + onClick = { + showMenu = false + onViewGithub() + }, + modifier = Modifier.testTag("menuItem_View on GitHub") + ) + DropdownMenuItem( + text = { Text("Toggle Values") }, + onClick = { + showMenu = false + showValues = !showValues + }, + modifier = Modifier.testTag("menuItem_Toggle Values") + ) + DropdownMenuItem( + text = { Text("Toggle Pinch Zoom") }, + onClick = { + showMenu = false + pinchZoom = !pinchZoom + }, + modifier = Modifier.testTag("menuItem_Toggle Pinch Zoom") + ) + DropdownMenuItem( + text = { Text("Toggle Auto Scale MinMax") }, + onClick = { + showMenu = false + autoScaleMinMax = !autoScaleMinMax + }, + modifier = Modifier.testTag("menuItem_Toggle Auto Scale MinMax") + ) + DropdownMenuItem( + text = { Text("Toggle Filled") }, + onClick = { + showMenu = false + showFilled = !showFilled + }, + modifier = Modifier.testTag("menuItem_Toggle Filled") + ) + DropdownMenuItem( + text = { Text("Toggle Circles") }, + onClick = { + showMenu = false + showCircles = !showCircles + }, + modifier = Modifier.testTag("menuItem_Toggle Circles") + ) + DropdownMenuItem( + text = { Text("Toggle Cubic") }, + onClick = { + showMenu = false + lineMode = when (lineMode) { + LineDataSet.Mode.CUBIC_BEZIER -> LineDataSet.Mode.LINEAR + else -> LineDataSet.Mode.CUBIC_BEZIER + } + }, + modifier = Modifier.testTag("menuItem_Toggle Cubic") + ) + DropdownMenuItem( + text = { Text("Toggle Stepped") }, + onClick = { + showMenu = false + lineMode = when (lineMode) { + LineDataSet.Mode.STEPPED -> LineDataSet.Mode.LINEAR + else -> LineDataSet.Mode.STEPPED + } + }, + modifier = Modifier.testTag("menuItem_Toggle Stepped") + ) + DropdownMenuItem( + text = { Text("Toggle Horizontal Cubic") }, + onClick = { + showMenu = false + lineMode = when (lineMode) { + LineDataSet.Mode.HORIZONTAL_BEZIER -> LineDataSet.Mode.LINEAR + else -> LineDataSet.Mode.HORIZONTAL_BEZIER + } + }, + modifier = Modifier.testTag("menuItem_Toggle Horizontal Cubic") + ) + DropdownMenuItem( + text = { Text("Save to Gallery") }, + onClick = { + showMenu = false + onSaveToGallery() + }, + modifier = Modifier.testTag("menuItem_Save to Gallery") + ) + } + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(Color.White) + ) { + // Chart - Using Compose LineChart + val lineData = remember( + seekBarXValue, + seekBarYValue, + showValues, + showFilled, + showCircles, + lineMode + ) { + createLineData( + seekBarXValue.toInt(), + seekBarYValue, + showValues, + showFilled, + showCircles, + lineMode + ) + } + + LineChart( + data = lineData, + modifier = Modifier + .fillMaxWidth() + .weight(1f), + drawGridBackground = false, + pinchZoomEnabled = pinchZoom, + autoScaleMinMax = autoScaleMinMax, + animationDuration = 1500, + onValueSelected = { entry, highlight -> + entry?.let { + Timber.i("Value: ${it.y}, xIndex: ${it.x}, DataSet index: ${highlight?.dataSetIndex}") + } + }, + xAxisConfig = { xAxis -> + xAxis.isDrawAxisLine = false + xAxis.isDrawGridLines = false + }, + leftAxisConfig = { leftAxis -> + leftAxis.isEnabled = false + }, + rightAxisConfig = { rightAxis -> + rightAxis.isDrawAxisLine = false + rightAxis.isDrawGridLines = false + }, + legend = { legend -> + legend.verticalAlignment = Legend.LegendVerticalAlignment.TOP + legend.horizontalAlignment = Legend.LegendHorizontalAlignment.RIGHT + legend.orientation = Legend.LegendOrientation.VERTICAL + legend.setDrawInside(false) + } + ) + + // SeekBar X with label + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "X:", + modifier = Modifier.padding(end = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + Slider( + value = seekBarXValue, + onValueChange = { newValue -> + seekBarXValue = newValue + }, + valueRange = 0f..100f, + modifier = Modifier.weight(1f) + ) + Text( + text = seekBarXValue.toInt().toString(), + modifier = Modifier.padding(start = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + } + + // SeekBar Y with label + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Y:", + modifier = Modifier.padding(end = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + Slider( + value = seekBarYValue, + onValueChange = { newValue -> + seekBarYValue = newValue + }, + valueRange = 0f..200f, + modifier = Modifier.weight(1f) + ) + Text( + text = seekBarYValue.toInt().toString(), + modifier = Modifier.padding(start = 8.dp), + style = MaterialTheme.typography.bodyMedium + ) + } + } + } + } + + private fun createLineData( + progress: Int, + range: Float, + showValues: Boolean, + showFilled: Boolean, + showCircles: Boolean, + lineMode: LineDataSet.Mode + ): LineData { + val dataSets = ArrayList() + + for (datasetNumber in 0..2) { + val values = ArrayList() + val sampleValues = when (datasetNumber) { + 1 -> getValues(100).reversedArray() + 2 -> generateSineWaves(3, 30).toTypedArray() + else -> getValues(100) + } + + for (i in 0..