diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 242167b..a9d3892 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) } android { @@ -16,7 +17,7 @@ android { } buildFeatures { - viewBinding = true + compose = true } buildTypes { @@ -44,15 +45,17 @@ kotlin { } } - dependencies { - // UI implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.androidx.constraintlayout) - implementation(libs.androidx.recyclerview) - implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.activity.compose) implementation(libs.androidx.lifecycle.viewmodel.ktx) - implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.compose.material3) implementation(libs.google.material) + + debugImplementation(platform(libs.androidx.compose.bom)) + debugImplementation(libs.androidx.compose.ui.tooling) } diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainActivity.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainActivity.kt index 09f8140..9a49c73 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainActivity.kt @@ -1,86 +1,26 @@ package com.juandiana.reflexgame.ui import android.os.Bundle -import android.view.MotionEvent -import android.view.View +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView import com.juandiana.reflexgame.app.ReflexGameApp -import com.juandiana.reflexgame.databinding.ActivityMainBinding -import com.juandiana.reflexgame.ui.model.MainUiState +import com.juandiana.reflexgame.ui.theme.ReflexGameTheme -class MainActivity : AppCompatActivity() { +class MainActivity : ComponentActivity() { private val viewModel by viewModels { MainViewModelFactory( ReflexGameApp.instance.resourcesProvider, ReflexGameApp.instance.gameEngine ) } - private lateinit var binding: ActivityMainBinding - private lateinit var squaresAdapter: SquaresAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) - squaresAdapter = SquaresAdapter() - binding.squaresGrid.apply { - adapter = squaresAdapter - layoutManager = object : GridLayoutManager(this@MainActivity, viewModel.columnCount) { - override fun canScrollVertically(): Boolean { - return false - } + setContent { + ReflexGameTheme { + MainScreenRoute(viewModel = viewModel) } - itemAnimator = null - addOnItemTouchListener(onItemTouchListener) - } - binding.button.setOnClickListener { viewModel.onButtonClicked() } - viewModel.uiState.observe(this, this::observeUiState) - } - - private fun observeUiState(uiState: MainUiState) { - with(binding) { - titleText.visibility = if (uiState.showTitle) View.VISIBLE else View.INVISIBLE - scoreValue.text = uiState.scoreText - button.text = uiState.buttonText - if (uiState.chronometerIsRunning) { - binding.elapsedTimeValue.base = uiState.chronometerStartTime - binding.elapsedTimeValue.start() - } else { - binding.elapsedTimeValue.stop() - } - } - squaresAdapter.submitList(uiState.squares) - } - - private val onItemTouchListener = object : RecyclerView.OnItemTouchListener { - override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { - return true - } - - override fun onTouchEvent(recyclerView: RecyclerView, motionEvent: MotionEvent) { - when (motionEvent.actionMasked) { - MotionEvent.ACTION_DOWN -> { - val itemView = recyclerView.findChildViewUnder(motionEvent.x, motionEvent.y) - if (itemView != null) { - val itemPosition = recyclerView.getChildAdapterPosition(itemView) - viewModel.onSquareTapped(index = itemPosition) - } - } - MotionEvent.ACTION_POINTER_DOWN -> { - val itemView = recyclerView.findChildViewUnder(motionEvent.getX(motionEvent.actionIndex), motionEvent.getY(motionEvent.actionIndex)) - if (itemView != null) { - val itemPosition = recyclerView.getChildAdapterPosition(itemView) - viewModel.onSquareTapped(index = itemPosition) - } - } - } - } - - override fun onRequestDisallowInterceptTouchEvent(disallowIntercept: Boolean) { - // Do nothing } } } diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt new file mode 100644 index 0000000..02d8063 --- /dev/null +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreen.kt @@ -0,0 +1,227 @@ +package com.juandiana.reflexgame.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.juandiana.reflexgame.R +import com.juandiana.reflexgame.ui.theme.ReflexGameTheme +import com.juandiana.reflexgame.ui.theme.spacing +import java.util.Locale + +@Composable +fun MainScreenRoute(viewModel: MainViewModel) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + MainScreen( + showTitle = uiState.showTitle, + squares = uiState.squares, + scoreText = uiState.scoreText, + buttonText = uiState.buttonText, + elapsedTimeText = uiState.elapsedTimeText, + columnCount = viewModel.columnCount, + onSquareTouched = viewModel::onSquareTouched, + onButtonClicked = viewModel::onButtonClicked + ) +} + +@Composable +fun MainScreen( + showTitle: Boolean, + squares: List, + scoreText: String, + buttonText: String, + elapsedTimeText: String, + columnCount: Int, + onSquareTouched: (Int) -> Unit, + onButtonClicked: () -> Unit +) { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(MaterialTheme.spacing.medium) + ) { + Text( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .alpha(if (showTitle) 1f else 0f), + text = stringResource(id = R.string.game_over).uppercase(Locale.getDefault()), + style = MaterialTheme.typography.displayLarge + ) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) + SquaresGrid( + modifier = Modifier.fillMaxWidth(), + squares = squares, + columnCount = columnCount, + onSquareTouched = onSquareTouched + ) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) + HeadsUpDisplay( + modifier = Modifier.fillMaxWidth(), + scoreText = scoreText, + elapsedTimeText = elapsedTimeText + ) + Spacer(modifier = Modifier.height(MaterialTheme.spacing.medium)) + Button( + onClick = onButtonClicked, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = buttonText, + fontWeight = FontWeight.Bold + ) + } + } + } +} + +@Composable +private fun SquaresGrid( + squares: List, + columnCount: Int, + onSquareTouched: (Int) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .background(Color.Black) + .padding(MaterialTheme.spacing.border) + ) { + squares.chunked(columnCount).forEachIndexed { rowIndex, rowSquares -> + Row(modifier = Modifier.fillMaxWidth()) { + rowSquares.forEachIndexed { columnIndex, squareState -> + val index = rowIndex * columnCount + columnIndex + SquareItem( + modifier = Modifier.weight(1f), + squareState = squareState, + onSquareTouched = { onSquareTouched(index) } + ) + } + repeat(columnCount - rowSquares.size) { + Spacer( + modifier = Modifier + .weight(1f) + .aspectRatio(1f) + ) + } + } + } + } +} + +@Composable +private fun HeadsUpDisplay( + scoreText: String, + elapsedTimeText: String, + modifier: Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = stringResource(id = R.string.score), + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) + Text( + modifier = Modifier.weight(1f), + text = scoreText + ) + Text( + text = stringResource(id = R.string.time), + fontWeight = FontWeight.Bold + ) + Spacer(modifier = Modifier.width(MaterialTheme.spacing.small)) + Text(text = elapsedTimeText) + } +} + +@Composable +private fun SquareItem( + squareState: SquareState, + onSquareTouched: () -> Unit, + modifier: Modifier = Modifier +) { + val border = MaterialTheme.spacing.border + val color = when (squareState) { + SquareState.CLEAR -> Color.White + SquareState.FLASHED -> Color.Blue + SquareState.TAPPED_SUCCESSFULLY -> Color.Green + SquareState.TAPPED_ERRONEOUSLY -> Color.Red + } + Box( + modifier = modifier + .aspectRatio(1f) + .padding(border) + .background(color) + .pointerInput(onSquareTouched) { + awaitPointerEventScope { + while (true) { + val event = awaitPointerEvent(pass = PointerEventPass.Initial) + if (event.changes.any { it.changedToDownIgnoreConsumed() }) { + onSquareTouched() + } + } + } + } + ) +} + +@Preview(showBackground = true) +@Composable +private fun MainScreenPreview() { + ReflexGameTheme { + MainScreen( + showTitle = false, + squares = listOf( + SquareState.CLEAR, + SquareState.FLASHED, + SquareState.CLEAR, + SquareState.TAPPED_SUCCESSFULLY, + SquareState.CLEAR, + SquareState.CLEAR, + SquareState.TAPPED_ERRONEOUSLY, + SquareState.CLEAR, + SquareState.FLASHED, + SquareState.CLEAR, + SquareState.CLEAR, + SquareState.CLEAR, + SquareState.CLEAR, + SquareState.CLEAR, + SquareState.FLASHED, + SquareState.CLEAR + ), + scoreText = "8", + buttonText = "Stop", + elapsedTimeText = "00:12", + columnCount = 4, + onSquareTouched = {}, + onButtonClicked = {} + ) + } +} diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt new file mode 100644 index 0000000..5c41098 --- /dev/null +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainScreenState.kt @@ -0,0 +1,16 @@ +package com.juandiana.reflexgame.ui + +enum class SquareState { + CLEAR, + FLASHED, + TAPPED_SUCCESSFULLY, + TAPPED_ERRONEOUSLY +} + +data class MainScreenState( + val showTitle: Boolean, + val squares: List, + val scoreText: String, + val buttonText: String, + val elapsedTimeText: String +) diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt index 49c941f..4a91c56 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModel.kt @@ -1,34 +1,38 @@ package com.juandiana.reflexgame.ui import android.os.SystemClock -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.text.format.DateUtils import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.juandiana.reflexgame.R import com.juandiana.reflexgame.app.ResourcesProvider import com.juandiana.reflexgame.domain.GameEngine -import com.juandiana.reflexgame.ui.model.MainUiState -import com.juandiana.reflexgame.ui.model.SquareState +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch class MainViewModel( private val resourcesProvider: ResourcesProvider, private val gameEngine: GameEngine ) : ViewModel() { - private val _uiState = MutableLiveData() - val uiState: LiveData get() = _uiState - - val columnCount = gameEngine.columns - - init { - _uiState.value = MainUiState( + private var chronometerStartTime: Long? = null + private var chronometerJob: Job? = null + private val _uiState = MutableStateFlow( + MainScreenState( showTitle = false, - squares = List(gameEngine.squaresCount) { SquareState.CLEAR }, + squares = clearSquares(), scoreText = resourcesProvider.getString(R.string.not_available), buttonText = resourcesProvider.getString(R.string.start), - chronometerStartTime = 0, - chronometerIsRunning = false + elapsedTimeText = formatElapsedTime(0L) ) - } + ) + val uiState: StateFlow = _uiState.asStateFlow() + + val columnCount = gameEngine.columns fun onButtonClicked() { if (gameEngine.isGameInProcess) { @@ -38,28 +42,57 @@ class MainViewModel( } } - fun onSquareTapped(index: Int) { + fun onSquareTouched(index: Int) { gameEngine.tapSquare(index) } override fun onCleared() { + stopChronometer() gameEngine.endGame() + super.onCleared() + } + + private fun startChronometer(startTime: Long) { + chronometerStartTime = startTime + chronometerJob?.cancel() + chronometerJob = viewModelScope.launch { + while (isActive) { + val elapsedSeconds = elapsedSecondsSince(startTime) + _uiState.value = _uiState.value.copy( + elapsedTimeText = formatElapsedTime(elapsedSeconds) + ) + delay(1000L) + } + } + } + + private fun stopChronometer() { + val startTime = chronometerStartTime + if (startTime != null) { + val elapsedSeconds = elapsedSecondsSince(startTime) + _uiState.value = _uiState.value.copy( + elapsedTimeText = formatElapsedTime(elapsedSeconds) + ) + } + chronometerJob?.cancel() + chronometerJob = null + chronometerStartTime = null } private val gameUpdatesListener = object : GameEngine.UpdatesListener { override fun onGameStarted(initialScore: Int) { - _uiState.value = MainUiState( + _uiState.value = MainScreenState( showTitle = false, - squares = List(gameEngine.squaresCount) { SquareState.CLEAR }, + squares = clearSquares(), scoreText = initialScore.toString(), buttonText = resourcesProvider.getString(R.string.stop), - chronometerStartTime = SystemClock.elapsedRealtime(), - chronometerIsRunning = true + elapsedTimeText = formatElapsedTime(0L) ) + startChronometer(startTime = SystemClock.elapsedRealtime()) } override fun onTurnStarted(flashedSquaresIndexes: Set) { - _uiState.value = _uiState.value!!.copy( + _uiState.value = _uiState.value.copy( squares = List(gameEngine.squaresCount) { index -> if (index in flashedSquaresIndexes) { SquareState.FLASHED @@ -71,7 +104,7 @@ class MainViewModel( } override fun onMarkSquareTap(flashedSquaresIndexes: Set, tappedSquaresIndexes: Set) { - _uiState.value = _uiState.value!!.copy( + _uiState.value = _uiState.value.copy( squares = List(gameEngine.squaresCount) { index -> if (index in tappedSquaresIndexes) { if (index in flashedSquaresIndexes) { @@ -89,16 +122,26 @@ class MainViewModel( } override fun onTurnEnded(score: Int) { - _uiState.value = _uiState.value!!.copy(scoreText = score.toString()) + _uiState.value = _uiState.value.copy(scoreText = score.toString()) } override fun onGameEnded() { - _uiState.value = _uiState.value!!.copy( + stopChronometer() + _uiState.value = _uiState.value.copy( showTitle = true, - squares = List(gameEngine.squaresCount) { SquareState.CLEAR }, - buttonText = resourcesProvider.getString(R.string.start), - chronometerIsRunning = false + squares = clearSquares(), + buttonText = resourcesProvider.getString(R.string.start) ) } } + + private fun clearSquares(): List = List(gameEngine.squaresCount) { SquareState.CLEAR } + + private fun elapsedSecondsSince(startTime: Long): Long { + return ((SystemClock.elapsedRealtime() - startTime) / 1000L).coerceAtLeast(0L) + } + + private fun formatElapsedTime(seconds: Long): String { + return DateUtils.formatElapsedTime(seconds) + } } diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModelFactory.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModelFactory.kt index b2b0c7b..a4a1c54 100644 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModelFactory.kt +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/MainViewModelFactory.kt @@ -11,6 +11,9 @@ class MainViewModelFactory( ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { - return MainViewModel(resourcesProvider, gameEngine) as T + if (modelClass.isAssignableFrom(MainViewModel::class.java)) { + return MainViewModel(resourcesProvider, gameEngine) as T + } + throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}") } } diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/SquaresAdapter.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/SquaresAdapter.kt deleted file mode 100644 index 26220b3..0000000 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/SquaresAdapter.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.juandiana.reflexgame.ui - -import android.graphics.Color -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.juandiana.reflexgame.databinding.ItemSquareBinding -import com.juandiana.reflexgame.ui.model.SquareState - -class SquaresAdapter : ListAdapter(SquareStateDiffUtil()) { - class ViewHolder(private val itemBinding: ItemSquareBinding) : RecyclerView.ViewHolder(itemBinding.root) { - fun bind(squareState: SquareState) { - with(itemBinding) { - root.setBackgroundColor( - when (squareState) { - SquareState.CLEAR -> Color.WHITE - SquareState.FLASHED -> Color.BLUE - SquareState.TAPPED_SUCCESSFULLY -> Color.GREEN - SquareState.TAPPED_ERRONEOUSLY -> Color.RED - } - ) - } - } - } - - class SquareStateDiffUtil : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: SquareState, newItem: SquareState): Boolean { - return oldItem == newItem - } - - override fun areContentsTheSame(oldItem: SquareState, newItem: SquareState): Boolean { - return oldItem == newItem - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val itemSquareBinding = ItemSquareBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolder(itemSquareBinding) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val squareState = getItem(position) - holder.bind(squareState) - } -} diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/livedata/Event.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/livedata/Event.kt deleted file mode 100644 index 352444e..0000000 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/livedata/Event.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.juandiana.reflexgame.ui.livedata - -/** - * Used as a wrapper for data that is exposed via a LiveData that represents an event. - */ -open class Event(private val content: T) { - - @Suppress("MemberVisibilityCanBePrivate") - var hasBeenHandled = false - private set // Allow external read but not write - - /** - * Returns the content and prevents its use again. - */ - fun getContentIfNotHandled(): T? { - return if (hasBeenHandled) { - null - } else { - hasBeenHandled = true - content - } - } - - /** - * Returns the content, even if it's already been handled. - */ - fun peekContent(): T = content -} diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/livedata/EventObserver.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/livedata/EventObserver.kt deleted file mode 100644 index 1059c98..0000000 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/livedata/EventObserver.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.juandiana.reflexgame.ui.livedata - -import androidx.lifecycle.Observer - -/** - * An [Observer] for [Event]s, simplifying the pattern of checking if the [Event]'s content has - * already been handled. - * - * [onEventUnhandledContent] is *only* called if the [Event]'s contents has not been handled. - */ -class EventObserver(private val onEventUnhandledContent: (T) -> Unit) : Observer> { - override fun onChanged(value: Event) { - value.getContentIfNotHandled()?.let { - onEventUnhandledContent(it) - } - } -} diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/model/MainUiState.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/model/MainUiState.kt deleted file mode 100644 index 6fa5bad..0000000 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/model/MainUiState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.juandiana.reflexgame.ui.model - -data class MainUiState( - val showTitle: Boolean, - val squares: List, - val scoreText: String, - val buttonText: String, - val chronometerStartTime: Long, - val chronometerIsRunning: Boolean -) diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/model/SquareState.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/model/SquareState.kt deleted file mode 100644 index 2c26203..0000000 --- a/app/src/main/kotlin/com/juandiana/reflexgame/ui/model/SquareState.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.juandiana.reflexgame.ui.model - -enum class SquareState { - CLEAR, - FLASHED, - TAPPED_SUCCESSFULLY, - TAPPED_ERRONEOUSLY -} diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Color.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Color.kt new file mode 100644 index 0000000..dde4d09 --- /dev/null +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Color.kt @@ -0,0 +1,8 @@ +package com.juandiana.reflexgame.ui.theme + +import androidx.compose.ui.graphics.Color + +internal val Purple500 = Color(0xFF6200EE) +internal val Teal200 = Color(0xFF03DAC5) +internal val Black = Color(0xFF000000) +internal val White = Color(0xFFFFFFFF) diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt new file mode 100644 index 0000000..9136913 --- /dev/null +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Spacing.kt @@ -0,0 +1,21 @@ +package com.juandiana.reflexgame.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Immutable +internal data class ReflexGameSpacing( + val border: Dp = 1.dp, + val small: Dp = 8.dp, + val medium: Dp = 16.dp +) + +internal val LocalReflexGameSpacing = staticCompositionLocalOf { ReflexGameSpacing() } + +internal val MaterialTheme.spacing: ReflexGameSpacing + @Composable + get() = LocalReflexGameSpacing.current diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt new file mode 100644 index 0000000..c505e88 --- /dev/null +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Theme.kt @@ -0,0 +1,26 @@ +package com.juandiana.reflexgame.ui.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider + +private val ReflexGameLightColorScheme = lightColorScheme( + primary = Purple500, + onPrimary = White, + secondary = Teal200, + onSecondary = Black +) + +@Composable +internal fun ReflexGameTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = ReflexGameLightColorScheme, + typography = ReflexGameTypography + ) { + CompositionLocalProvider( + LocalReflexGameSpacing provides ReflexGameSpacing(), + content = content + ) + } +} diff --git a/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Type.kt b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Type.kt new file mode 100644 index 0000000..80c59ff --- /dev/null +++ b/app/src/main/kotlin/com/juandiana/reflexgame/ui/theme/Type.kt @@ -0,0 +1,13 @@ +package com.juandiana.reflexgame.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +internal val ReflexGameTypography = Typography( + displayLarge = TextStyle( + fontSize = 32.sp, + fontWeight = FontWeight.Bold + ) +) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 77e8128..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_square.xml b/app/src/main/res/layout/item_square.xml deleted file mode 100644 index 402cbd3..0000000 --- a/app/src/main/res/layout/item_square.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index 648cfde..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml deleted file mode 100644 index 144fccb..0000000 --- a/app/src/main/res/values/dimens.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - 1dp - 8dp - 16dp - 32sp - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index de42fc4..babc240 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,14 +1,5 @@ diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 137f706..cdd9216 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,22 +1,24 @@ [versions] android-gradle-plugin = "9.0.1" +kotlin = "2.3.10" androidx-core-ktx = "1.17.0" -androidx-appcompat = "1.7.1" -androidx-constraintlayout = "2.2.1" -androidx-recyclerview = "1.4.0" -androidx-fragment-ktx = "1.8.9" +androidx-activity = "1.12.4" androidx-lifecycle = "2.10.0" +androidx-compose-bom = "2026.02.00" google-material = "1.13.0" [plugins] android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } [libraries] androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core-ktx" } -androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } -androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } -androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerview" } -androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment-ktx" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } -androidx-lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-ui = { module = "androidx.compose.ui:ui" } +androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } +androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } +androidx-compose-material3 = { module = "androidx.compose.material3:material3" } google-material = { module = "com.google.android.material:material", version.ref = "google-material" }