diff --git a/EUPHONY_UI.md b/EUPHONY_UI.md new file mode 100644 index 0000000..0c8ee83 --- /dev/null +++ b/EUPHONY_UI.md @@ -0,0 +1,51 @@ +# Euphony Ui + +## Introduction + +Euphony provides basic UI components for easy use. These work based on Jetpack Compose. + +## How to use + +1. [Add Jetpack Compose](https://developer.android.com/jetpack/compose/interop/adding) to your + project. +2. Declare Euphony UI where necessary + ```kotlin + @Composable + fun MainScreen() { + Column( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 40.dp) + ) { + EuphonyTxPanel( + hint = "hint text", + textColor = Red, + panelHeight = 60.dp + ) + } + } + ``` + +## Components + +### EuphonyTxPanel + +Write text to transmit and click button to create sound waves. + +#### Preview + + + +#### Options + +|Name|Type|Description| +|---|---|---| +|hint|String|Hint text in the the text field.| +|textColor|Color|Text color of the text field. Default color is Black.| +|hintTextColor|Color|Text color of the hint text. Default color is LightGray.| +|textFieldBackgroundColor|Color|Background color of the text field. Default color is LightSkyBlue.| +|buttonBackgroundColor|Color|Background color of the right side button. Default color is LightBlue.| +|panelHeight|Dp|Height of this component.| +|cornerRadius|Dp|Corner radius of this component.| +|iconTintColor|Color|Icon color of the right side button. Default color is White.| diff --git a/README.md b/README.md index c3c9146..6b091aa 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ mRxManager.listen(); //Listening Start Below links are detail guides. - [Getting Started Guide](GETTING_STARTED.md) - [How To Build & Unit Test Guide](HOWTOBUILD.md) +- [Euphony Ui](EUPHONY_UI.md) ## Architecture

euphony architecture

diff --git a/assets/eutxpanel_screenshot.png b/assets/eutxpanel_screenshot.png new file mode 100644 index 0000000..d971872 Binary files /dev/null and b/assets/eutxpanel_screenshot.png differ diff --git a/euphony/src/androidTest/java/co/euphony/ui/EuphonyTxPanelKtTest.kt b/euphony/src/androidTest/java/co/euphony/ui/EuphonyTxPanelKtTest.kt new file mode 100644 index 0000000..e71b9f1 --- /dev/null +++ b/euphony/src/androidTest/java/co/euphony/ui/EuphonyTxPanelKtTest.kt @@ -0,0 +1,47 @@ +package co.euphony.ui + +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import co.euphony.common.Constants +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.junit.MockitoJUnitRunner + + +@ExperimentalMaterialApi +@ExperimentalComposeUiApi +@RunWith(MockitoJUnitRunner::class) +class EuphonyTxPanelKtTest { + + @get:Rule + val composeTestRule = createComposeRule() + + @Before + fun setup() { + composeTestRule.setContent { + EuphonyTxPanel( + hint = "hint text" + ) + } + } + + @Test + fun haveEmptyValueAtFirst() { + composeTestRule.onNodeWithText("hint text").assertExists() + composeTestRule.onNodeWithContentDescription(Constants.TAG_TX_ICON_START).assertExists() + } + + @Test + fun startSendTextAndFinishSending() { + composeTestRule.onNodeWithText("").performTextInput("Text") + composeTestRule.onNodeWithTag(Constants.TAG_TX_BTN).performClick() + composeTestRule.onNodeWithContentDescription(Constants.TAG_TX_ICON_STOP).assertExists() + + composeTestRule.onNodeWithTag(Constants.TAG_TX_BTN).performClick() + composeTestRule.onNodeWithContentDescription(Constants.TAG_TX_ICON_START).assertExists() + } +} \ No newline at end of file diff --git a/euphony/src/main/java/co/euphony/common/Constants.java b/euphony/src/main/java/co/euphony/common/Constants.java index 50d24a1..d052663 100644 --- a/euphony/src/main/java/co/euphony/common/Constants.java +++ b/euphony/src/main/java/co/euphony/common/Constants.java @@ -14,12 +14,17 @@ public class Constants { public static final int START_SIGNAL_FREQ = STANDARD_FREQ - CHANNEL_INTERVAL; public static final int BUNDLE_INTERVAL = CHANNEL_INTERVAL * CHANNEL; - + // RX public static final int MAX_REF = 4000; public static final int MIN_REF = 50; - public static final int DEFAULT_REF = 500; // BASE Reference value + public static final int DEFAULT_REF = 500; // BASE Reference value + + // EuphonyTxPanel Test Tags + public static final String TAG_TX_BTN = "EuphonyTxPanelButton"; + public static final String TAG_TX_ICON_STOP = "EuphonyTxPanelIcon_Stop"; + public static final String TAG_TX_ICON_START = "EuphonyTxPanelIcon_Start"; // TxRxChecker Test Tags public static final String TAG_INPUT = "TxRxCheckerInput"; diff --git a/euphony/src/main/java/co/euphony/ui/EuphonyTxPanel.kt b/euphony/src/main/java/co/euphony/ui/EuphonyTxPanel.kt new file mode 100644 index 0000000..ede30ec --- /dev/null +++ b/euphony/src/main/java/co/euphony/ui/EuphonyTxPanel.kt @@ -0,0 +1,100 @@ +package co.euphony.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Send +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Color.Companion.Black +import androidx.compose.ui.graphics.Color.Companion.LightGray +import androidx.compose.ui.graphics.Color.Companion.White +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import co.euphony.common.Constants.* +import co.euphony.tx.EuTxManager +import co.euphony.ui.theme.LightBlue +import co.euphony.ui.theme.LightSkyBlue +import co.euphony.ui.viewmodel.TxPanelViewModel +import co.euphony.ui.viewmodel.TxPanelViewModelFactory + +@Composable +fun EuphonyTxPanel( + hint: String = "", + textColor: Color = Black, + hintTextColor: Color = LightGray, + textFieldBackgroundColor: Color = LightSkyBlue, + buttonBackgroundColor: Color = LightBlue, + panelHeight: Dp = 54.dp, + cornerRadius: Dp = 8.dp, + iconTintColor: Color = White +) { + val viewModel: TxPanelViewModel = + viewModel(factory = TxPanelViewModelFactory(EuTxManager.getInstance())) + + val btnStatus = viewModel.isProcessing.collectAsState() + var textData by remember { mutableStateOf("") } + + Row( + modifier = Modifier + .fillMaxWidth() + .height(panelHeight), + verticalAlignment = Alignment.CenterVertically + ) { + TextField( + modifier = Modifier + .weight(1f) + .height(panelHeight), + value = textData, + onValueChange = { + textData = it + }, + shape = RoundedCornerShape(topStart = cornerRadius, bottomStart = cornerRadius), + colors = TextFieldDefaults.textFieldColors( + textColor = textColor, + backgroundColor = textFieldBackgroundColor, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + placeholder = { + Text( + text = hint, + color = hintTextColor + ) + } + ) + Button( + onClick = { + viewModel.onBtnClick(textData) + }, + modifier = Modifier + .height(panelHeight) + .testTag(TAG_TX_BTN), + shape = RoundedCornerShape(bottomEnd = cornerRadius, topEnd = cornerRadius), + colors = ButtonDefaults.buttonColors(backgroundColor = buttonBackgroundColor), + elevation = null, + ) { + Icon( + imageVector = if (btnStatus.value) { + Icons.Default.Close + } else { + Icons.Default.Send + }, + contentDescription = if (btnStatus.value) { + TAG_TX_ICON_STOP + } else { + TAG_TX_ICON_START + }, + tint = iconTintColor, + ) + } + } +} diff --git a/euphony/src/main/java/co/euphony/ui/viewmodel/TxPanelViewModel.kt b/euphony/src/main/java/co/euphony/ui/viewmodel/TxPanelViewModel.kt new file mode 100644 index 0000000..7082734 --- /dev/null +++ b/euphony/src/main/java/co/euphony/ui/viewmodel/TxPanelViewModel.kt @@ -0,0 +1,51 @@ +package co.euphony.ui.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import co.euphony.tx.EuTxManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class TxPanelViewModel( + private val txManager: EuTxManager +) : ViewModel() { + + private val _isProcessing = MutableStateFlow(false) + val isProcessing: StateFlow = _isProcessing + + fun onBtnClick(data: String) { + if (_isProcessing.value) { + stop() + } else { + start(data) + } + } + + fun stop() { + txManager.stop() + if (_isProcessing.value) { + _isProcessing.value = false + } + } + + private fun start(data: String) { + txManager.code = data + txManager.play(-1) + _isProcessing.value = true + } + + override fun onCleared() { + super.onCleared() + txManager.stop() + _isProcessing.value = false + } +} + +class TxPanelViewModelFactory( + private val euTxManager: EuTxManager +) : ViewModelProvider.NewInstanceFactory() { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + TxPanelViewModel(euTxManager) as T +} \ No newline at end of file diff --git a/euphony/src/test/java/co/euphony/ui/viewmodel/TxPanelViewModelTest.kt b/euphony/src/test/java/co/euphony/ui/viewmodel/TxPanelViewModelTest.kt new file mode 100644 index 0000000..e8c9231 --- /dev/null +++ b/euphony/src/test/java/co/euphony/ui/viewmodel/TxPanelViewModelTest.kt @@ -0,0 +1,46 @@ +package co.euphony.ui.viewmodel + +import co.euphony.tx.EuTxManager +import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.junit.MockitoJUnitRunner + +@RunWith(MockitoJUnitRunner::class) +class TxPanelViewModelTest { + + private lateinit var viewModel: TxPanelViewModel + + @Mock + private lateinit var txManager: EuTxManager + + @Before + fun setUp() { + viewModel = TxPanelViewModel(txManager) + } + + @After + fun tearDown() { + viewModel.stop() + } + + @Test + fun `if onBtnClick starts, isProcessing becomes true`() { + assertFalse(viewModel.isProcessing.value) + viewModel.onBtnClick("") + assertTrue(viewModel.isProcessing.value) + } + + @Test + fun `if stop() is called, isProcessing becomes false`() { + viewModel.onBtnClick("") + assertTrue(viewModel.isProcessing.value) + + viewModel.stop() + assertFalse(viewModel.isProcessing.value) + } +} \ No newline at end of file