Refactored compose ui and states

This commit is contained in:
Assasinnys 2022-11-18 00:47:52 +03:00
parent 75baf06c46
commit 266e5eb360
8 changed files with 117 additions and 81 deletions

View File

@ -1,5 +0,0 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation
import kotlin.math.roundToInt
fun Float.classicRound(): Float = times(100).roundToInt().div(100f)

View File

@ -1,14 +1,9 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation package com.dmitryzenevich.remoteaudiocontrol.presentation
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.*
import com.dmitryzenevich.remoteaudiocontrol.presentation.theme.RemoteAudioControlTheme import com.dmitryzenevich.remoteaudiocontrol.presentation.theme.RemoteAudioControlTheme
import androidx.lifecycle.viewmodel.compose.viewModel
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint @AndroidEntryPoint
@ -23,23 +18,3 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
val uiState by viewModel.uiState.collectAsState()
LazyRow {
items(
items = uiState.volumes,
key = { item -> item.pid }
) { itemState ->
VolumeItem(
volumeItemState = itemState,
onValueChanged = { newValue: Float ->
Log.i("debug", "new value: $newValue")
viewModel.onVolumeChanged(itemState, newValue.toInt())
}
)
}
}
}

View File

@ -0,0 +1,31 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@Composable
fun MainScreen(viewModel: MainViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
val uiState by viewModel.uiState.collectAsState()
LazyRow(
modifier = Modifier.fillMaxSize()
) {
items(
items = uiState.volumes,
key = { item -> item.pid }
) { itemState ->
VolumeItem(
volumeItemState = itemState,
onValueChanged = { newValue: Float ->
viewModel.onVolumeChanged(itemState, newValue.toInt())
},
onMuteClick = { viewModel.onMuteClick(itemState) }
)
}
}
}

View File

@ -0,0 +1,19 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
data class MainScreenUiState(
val volumes: SnapshotStateList<VolumeItemState> = mutableStateListOf(),
val isError: Boolean = false
)
data class VolumeItemState(
val pid: Long,
val name: String = "",
val volume: MutableState<Int> = mutableStateOf(0),
val isMuted: MutableState<Boolean> = mutableStateOf(false),
val isActive: Boolean = false
)

View File

@ -1,11 +1,6 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation package com.dmitryzenevich.remoteaudiocontrol.presentation
import android.util.Log import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.dmitryzenevich.remoteaudiocontrol.data.SocketRepositoryImpl import com.dmitryzenevich.remoteaudiocontrol.data.SocketRepositoryImpl
@ -15,6 +10,8 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import javax.inject.Inject import javax.inject.Inject
const val NOT_BLOCKED = -1L
@HiltViewModel @HiltViewModel
class MainViewModel @Inject constructor( class MainViewModel @Inject constructor(
private val socketRepositoryImpl: SocketRepositoryImpl private val socketRepositoryImpl: SocketRepositoryImpl
@ -23,7 +20,9 @@ class MainViewModel @Inject constructor(
private val _uiState = MutableStateFlow(MainScreenUiState()) private val _uiState = MutableStateFlow(MainScreenUiState())
val uiState = _uiState.asStateFlow() val uiState = _uiState.asStateFlow()
private var commandJob: Job? = null @Volatile
private var blockingPid: Long = NOT_BLOCKED
private var blockingJob: Job? = null
init { init {
viewModelScope.launch { viewModelScope.launch {
@ -33,7 +32,6 @@ class MainViewModel @Inject constructor(
.filterNotNull() .filterNotNull()
.onEach { .onEach {
Log.i(javaClass.simpleName, "received: $it") Log.i(javaClass.simpleName, "received: $it")
// TODO: test it
withContext(Dispatchers.Main) { proceedEvent(it) } withContext(Dispatchers.Main) { proceedEvent(it) }
} }
.catch { Log.e(javaClass.simpleName, "Fetch event error", it) } .catch { Log.e(javaClass.simpleName, "Fetch event error", it) }
@ -49,11 +47,7 @@ class MainViewModel @Inject constructor(
is NewSessionEvent -> addIfNotExist(event) is NewSessionEvent -> addIfNotExist(event)
is MuteStateChangedEvent -> { is MuteStateChangedEvent -> {
volumes.find { event.PID == it.pid }?.let { item -> volumes.find { event.PID == it.pid }?.let { item ->
val newItem = item.copy(isMuted = event.isMuted) item.isMuted.value = event.isMuted
volumes.set(
index = volumes.indexOfFirst { it.pid == newItem.pid },
element = newItem
)
} }
} }
is SetNameEvent -> { is SetNameEvent -> {
@ -75,28 +69,40 @@ class MainViewModel @Inject constructor(
} }
} }
is VolumeChangedEvent -> { is VolumeChangedEvent -> {
if (blockingPid == event.PID) return
volumes.find { event.PID == it.pid }?.let { item -> volumes.find { event.PID == it.pid }?.let { item ->
// val newItem = item.copy(volume = event.volume)
// volumes.set(
// index = volumes.indexOfFirst { it.pid == newItem.pid },
// element = newItem
// )
item.volume.value = event.volume item.volume.value = event.volume
blockVolumeEvent(event.PID)
} }
} }
UnknownEvent -> _uiState.value.isError // TODO: implement error state UnknownEvent -> _uiState.value.isError // TODO: implement error state
} }
} }
private fun blockVolumeEvent(eventPid: Long) {
blockingJob?.cancel()
blockingJob = viewModelScope.launch {
blockingPid = eventPid
delay(200)
blockingPid = NOT_BLOCKED
}
}
fun onVolumeChanged(volumeItemState: VolumeItemState, newVolume: Int) { fun onVolumeChanged(volumeItemState: VolumeItemState, newVolume: Int) {
Log.i(javaClass.simpleName, "old volume: ${volumeItemState.volume}, newVolume: $newVolume") Log.i(javaClass.simpleName, "old volume: ${volumeItemState.volume}, newVolume: $newVolume")
volumeItemState.volume.value = newVolume volumeItemState.volume.value = newVolume
sendCommand(SetVolumeCommand(volumeItemState.pid, newVolume)) sendCommand(SetVolumeCommand(volumeItemState.pid, newVolume))
} }
fun onMuteClick(volumeItemState: VolumeItemState) {
Log.i(javaClass.simpleName, "new checked: ${!volumeItemState.isMuted.value}")
volumeItemState.isMuted.value = !volumeItemState.isMuted.value
sendCommand(MuteToggleCommand(volumeItemState.pid))
}
private fun sendCommand(command: Command) { private fun sendCommand(command: Command) {
commandJob?.cancel() viewModelScope.launch { socketRepositoryImpl.sendCommand(command) }
commandJob = viewModelScope.launch { socketRepositoryImpl.sendCommand(command) }
} }
private fun addIfNotExist(event: NewSessionEvent) { private fun addIfNotExist(event: NewSessionEvent) {
@ -104,22 +110,6 @@ class MainViewModel @Inject constructor(
_uiState.value.volumes.add(event.toVolumeItemState()) _uiState.value.volumes.add(event.toVolumeItemState())
} }
} }
} }
class MainScreenUiState(
val volumes: SnapshotStateList<VolumeItemState> = mutableStateListOf(),
val isError: Boolean = false
)
data class VolumeItemState(
val pid: Long,
val name: String = "",
val volume: MutableState<Int> = mutableStateOf(0),
val isMuted: Boolean = false,
val isActive: Boolean = false
)
fun NewSessionEvent.toVolumeItemState() = VolumeItemState(pid = PID) fun NewSessionEvent.toVolumeItemState() = VolumeItemState(pid = PID)
//fun VolumeChangedEvent.toVolumeItemState() = VolumeItemState(pid = PID, volume = volume)

View File

@ -1,50 +1,66 @@
package com.dmitryzenevich.remoteaudiocontrol.presentation package com.dmitryzenevich.remoteaudiocontrol.presentation
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding import androidx.compose.material3.*
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.dmitryzenevich.remoteaudiocontrol.R
@Composable @Composable
fun VolumeItem( fun VolumeItem(
volumeItemState: VolumeItemState, volumeItemState: VolumeItemState,
onValueChanged: (Float) -> Unit onValueChanged: (Float) -> Unit,
onMuteClick: () -> Unit,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(8.dp) .fillMaxHeight()
.wrapContentSize(), .padding(8.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top
) { ) {
Text( Text(text = volumeItemState.name)
text = "${volumeItemState.name}\nVolume: ${volumeItemState.volume.value}" Text(text = volumeItemState.volume.value.toString())
)
VerticalSlider( VerticalSlider(
value = volumeItemState.volume.value.toFloat(), value = volumeItemState.volume.value.toFloat(),
onValueChanged = onValueChanged onValueChanged = onValueChanged,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) )
IconButton(onClick = onMuteClick) {
if (volumeItemState.isMuted.value)
Icon(
painter = painterResource(R.drawable.ic_volume_off),
contentDescription = "Muted"
)
else
Icon(
painter = painterResource(R.drawable.ic_volume_up),
contentDescription = "Resumed"
)
}
} }
} }
@Composable @Composable
fun VerticalSlider( fun VerticalSlider(
value: Float, value: Float,
onValueChanged: (Float) -> Unit onValueChanged: (Float) -> Unit,
modifier: Modifier = Modifier
) { ) {
Slider( Slider(
value = value, value = value,
valueRange = 0f..100f, valueRange = 0f..100f,
onValueChange = onValueChanged, onValueChange = onValueChanged,
modifier = Modifier modifier = modifier
.graphicsLayer { .graphicsLayer {
rotationZ = 270f rotationZ = 270f
transformOrigin = TransformOrigin(0f, 0f) transformOrigin = TransformOrigin(0f, 0f)
@ -62,6 +78,6 @@ fun VerticalSlider(
placeable.place(-placeable.width, 0) placeable.place(-placeable.width, 0)
} }
} }
.padding(horizontal = 24.dp) .padding(horizontal = 8.dp)
) )
} }

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
</vector>

View File

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
</vector>