Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat/#12] 공통 컴포넌트 button 구현 #13

Merged
merged 16 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
package com.spoony.spoony.core.designsystem.component.button

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.spoony.spoony.R
import com.spoony.spoony.core.designsystem.theme.SpoonyAndroidTheme
import com.spoony.spoony.core.designsystem.type.ButtonSize
import com.spoony.spoony.core.designsystem.type.ButtonStyle

@Composable
fun SpoonyButton(
text: String,
size: ButtonSize,
style: ButtonStyle,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
icon: (@Composable () -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
) {
val isPressed by interactionSource.collectIsPressedAsState()
val spoonyColors = SpoonyAndroidTheme.colors
val spoonyTypography = SpoonyAndroidTheme.typography
val backgroundColor = remember(enabled, isPressed, style) {
when {
!enabled -> when (style) {
ButtonStyle.Primary -> spoonyColors.main100
ButtonStyle.Secondary -> spoonyColors.gray300
ButtonStyle.Tertiary -> spoonyColors.gray100
}
isPressed -> when (style) {
ButtonStyle.Primary -> spoonyColors.main500
ButtonStyle.Secondary -> spoonyColors.gray800
ButtonStyle.Tertiary -> spoonyColors.gray100
}
else -> when (style) {
ButtonStyle.Primary -> spoonyColors.main400
ButtonStyle.Secondary -> spoonyColors.black
ButtonStyle.Tertiary -> spoonyColors.gray0
}
}
}
val paddingValues = remember(size) {
when (size) {
ButtonSize.Xlarge, ButtonSize.Large, ButtonSize.Medium, ButtonSize.Small -> PaddingValues(horizontal = 16.dp, vertical = 18.dp)
ButtonSize.Xsmall -> PaddingValues(horizontal = 16.dp, vertical = 12.dp)
}
}

val textColor = remember(style) {
when (style) {
ButtonStyle.Tertiary -> when {
!enabled -> spoonyColors.gray400
else -> spoonyColors.gray600
}
else -> spoonyColors.white
}
}

val cornerRadius = remember(size) {
when (size) {
ButtonSize.Xlarge -> 8.dp
ButtonSize.Large, ButtonSize.Medium, ButtonSize.Small, ButtonSize.Xsmall -> 10.dp
}
}
Row(
modifier = modifier
.clip(RoundedCornerShape(cornerRadius))
.background(color = backgroundColor)
.clickable(
enabled = enabled,
indication = null,
interactionSource = interactionSource,
onClick = onClick
)
.padding(paddingValues),
verticalAlignment = Alignment.CenterVertically,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p4:

.semantics { 
    contentDescription = "..." 
}

이런 접근성 챙겨주는것도 좋아보여요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오,, 처음 봤습니다! Row도 contentDescription 를 추가할 수 있었군요! 아직 어떤식으로 작성해야될지 모르겠어서 추후에 추가하도록 하겠습니다!

horizontalArrangement = Arrangement.Center
) {
if (icon != null) {
icon()
Spacer(modifier = Modifier.width(8.dp))
}
Text(
text = text,
color = textColor,
style = SpoonyAndroidTheme.typography.body2b
)
}
}

private class ButtonStyleProvider : PreviewParameterProvider<ButtonStyle> {
override val values: Sequence<ButtonStyle> = sequenceOf(
ButtonStyle.Primary,
ButtonStyle.Secondary,
ButtonStyle.Tertiary
)
}

@Preview
@Composable
fun SpoonyButtonEnabledPreview(
@PreviewParameter(ButtonStyleProvider::class) style: ButtonStyle
) {
SpoonyAndroidTheme {
Column(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ButtonSize.entries.forEach { size ->
SpoonyButton(
text = "버튼",
style = style,
size = size,
onClick = { }
)
}
}
}
}

@Preview
@Composable
fun SpoonyButtonEnabledIconPreview(
@PreviewParameter(ButtonStyleProvider::class) style: ButtonStyle
) {
SpoonyAndroidTheme {
Column(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ButtonSize.entries.forEach { size ->
SpoonyButton(
text = "버튼",
style = style,
size = size,
onClick = { },
icon = {
Icon(
painter = painterResource(R.drawable.ic_launcher_foreground),
modifier = Modifier.size(32.dp),
contentDescription = "ic_spoon_button",
tint = Color.Unspecified
)
}
)
}
}
}
}

@Preview
@Composable
fun SpoonyButtonDisabledPreview(
@PreviewParameter(ButtonStyleProvider::class) style: ButtonStyle
) {
SpoonyAndroidTheme {
Column(
modifier = Modifier,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
ButtonSize.entries.forEach { size ->
SpoonyButton(
text = "버튼",
style = style,
size = size,
enabled = false,
onClick = { }
)
}
}
}
}

@Preview
@Composable
private fun SpoonyButtonWidthModifierPreview() {
SpoonyAndroidTheme {
SpoonyButton(
text = "떠먹으러 가기",
onClick = {},
style = ButtonStyle.Secondary,
size = ButtonSize.Xsmall,
modifier = Modifier.width(134.dp)
)
}
}

@Preview
@Composable
private fun SpoonyButtonTwoButtonPreview() {
SpoonyAndroidTheme {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
SpoonyButton(
text = "버튼",
onClick = {},
style = ButtonStyle.Tertiary,
size = ButtonSize.Xsmall,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(13.dp))
SpoonyButton(
text = "버튼",
onClick = {},
style = ButtonStyle.Secondary,
size = ButtonSize.Xsmall,
modifier = Modifier.weight(1f)
)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
SpoonyButton(
text = "버튼",
onClick = {},
style = ButtonStyle.Tertiary,
size = ButtonSize.Xsmall,
enabled = false,
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(13.dp))
SpoonyButton(
text = "버튼",
onClick = {},
style = ButtonStyle.Secondary,
size = ButtonSize.Xsmall,
enabled = false,
modifier = Modifier.weight(1f)
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.spoony.spoony.core.designsystem.type

enum class ButtonSize {
Xlarge, Large, Medium, Small, Xsmall
}

enum class ButtonStyle {
Primary, Secondary, Tertiary
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.spoony.spoony.core.util.extension

import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed

@Composable
inline fun Modifier.noRippleClickable(crossinline onClick: () -> Unit): Modifier = composed {
this.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
onClick = { onClick() }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p3) 이 부분 이렇게도 되지 않나요? 불필요한 람다를 추가로 만들필요는 없을 것 같습니다.

Suggested change
onClick = { onClick() }
onClick = onClick

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

람다 전달과 호출의 차이

람다 전달:

onClick = onClick

여기서 onClick은 람다를 전달만 하고 호출은 나중에 이뤄집니다.
clickable 내부에서 이 람다를 호출할 때까지 실행되지 않습니다.

람다 호출:

onClick = { onClick() }

전달받은 onClick 람다를 호출하도록 명시적으로 정의합니다.
{} 내부에서 실행 로직을 추가하거나 호출을 제어할 수 있습니다.

왜 inline과 crossinline을 사용했는가?

inline: 함수 호출 오버헤드를 줄이고, noRippleClickable 함수가 자주 호출될 때 성능을 최적화.
전달받은 onClick 람다에 대한 익명 객체 생성을 방지.
crossinline: onClick에서 비지역 반환이 발생할 가능성을 방지.
예를 들어, return을 사용해 noRippleClickable 바깥의 함수를 종료하려고 할 때 오류를 방지.

결론

전달받은 람다를 명시적으로 호출해야 하기 때문에 onClick 을 사용하지 않고 { onClick() } 로 해야된다고 합니다.

image

일단 잘몰라서 정리해봤는데.. 아직도 사실 이해가 잘 가진 않습니다.

Copy link
Member

@chattymin chattymin Jan 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분은 저도 잘 모르던 부분인데 찾아보고 공유하도록 하겠습니다!
덕분에 새로운걸 알아가네요 :)

)
}
Loading