diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md index 2ed2f6eee..257b971ff 100644 --- a/.github/CODE_OF_CONDUCT.md +++ b/.github/CODE_OF_CONDUCT.md @@ -2,14 +2,9 @@ ## Our Pledge -We as members, contributors, and leaders pledge to make participation in our community a -harassment-free experience for everyone, regardless of age, body size, visible or invisible -disability, ethnicity, sex characteristics, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, race, religion, or sexual -identity and orientation. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and -healthy community. +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. ## Our Standards @@ -18,8 +13,7 @@ Examples of behavior that contributes to a positive environment for our communit * Demonstrating empathy and kindness toward other people * Being respectful of differing opinions, viewpoints, and experiences * Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the - experience +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience * Focusing on what is best not just for us as individuals, but for the overall community Examples of unacceptable behavior include: @@ -27,87 +21,59 @@ Examples of unacceptable behavior include: * The use of sexualized language or imagery, and sexual attention or advances of any kind * Trolling, insulting or derogatory comments, and personal or political attacks * Public or private harassment -* Publishing others' private information, such as a physical or email address, without their - explicit permission +* Publishing others' private information, such as a physical or email address, without their explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Enforcement Responsibilities -Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior -and will take appropriate and fair corrective action in response to any behavior that they deem -inappropriate, threatening, offensive, or harmful. +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, -code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and -will communicate reasons for moderation decisions when appropriate. +Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. ## Scope -This Code of Conduct applies within all community spaces, and also applies when an individual is -officially representing the community in public spaces. Examples of representing our community -include using an official e-mail address, posting via an official social media account, or acting as -an appointed representative at an online or offline event. +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. ## Enforcement -Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community -leaders responsible for enforcement at ji@sungb.in. All complaints will be reviewed and investigated -promptly and fairly. +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ji@sungb.in. All complaints will be reviewed and investigated promptly and fairly. -All community leaders are obligated to respect the privacy and security of the reporter of any -incident. +All community leaders are obligated to respect the privacy and security of the reporter of any incident. ## Enforcement Guidelines -Community leaders will follow these Community Impact Guidelines in determining the consequences for -any action they deem in violation of this Code of Conduct: +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: ### 1. Correction -**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or -unwelcome in the community. +**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. -**Consequence**: A private, written warning from community leaders, providing clarity around the -nature of the violation and an explanation of why the behavior was inappropriate. A public apology -may be requested. +**Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. ### 2. Warning **Community Impact**: A violation through a single incident or series of actions. -**Consequence**: A warning with consequences for continued behavior. No interaction with the people -involved, including unsolicited interaction with those enforcing the Code of Conduct, for a -specified period of time. This includes avoiding interactions in community spaces as well as -external channels like social media. Violating these terms may lead to a temporary or permanent ban. +**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. ### 3. Temporary Ban -**Community Impact**: A serious violation of community standards, including sustained inappropriate -behavior. +**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. -**Consequence**: A temporary ban from any sort of interaction or public communication with the -community for a specified period of time. No public or private interaction with the people involved, -including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this -period. Violating these terms may lead to a permanent ban. +**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. ### 4. Permanent Ban -**Community Impact**: Demonstrating a pattern of violation of community standards, including -sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement -of classes of individuals. +**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. **Consequence**: A permanent ban from any sort of public interaction within the community. ## Attribution -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at -https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. -Community Impact Guidelines were inspired -by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). +Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). [homepage]: https://www.contributor-covenant.org -For answers to common questions about this code of conduct, see the FAQ at -https://www.contributor-covenant.org/faq. Translations are available at -https://www.contributor-covenant.org/translations. +For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 2436298f9..2e4f13d62 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,7 +1,6 @@ ## How to contribute -We'd love to accept your patches and contributions to this project. There are just a few small -guidelines you need to follow. +We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Preparing a pull request for review @@ -26,7 +25,5 @@ Finally, you need to make sure the project builds successfully: ## Code reviews -All submissions, including submissions by project members, require review. We use GitHub pull -requests for this purpose. -Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) -for more information on using pull requests. +All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. +Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 16e3cbb8b..cf61cd15f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,10 +4,10 @@ ## Overview (Required) -- +- ## Screenshot -Before | After -:--: | :--: - | +| Before | After | +| :--: | :--: | +| | | diff --git a/.gitignore b/.gitignore index bbc36035e..65972bf9f 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ keystore /documents/dokka/ /report /ui-components-snapshots/out/ +snapshots \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b22cd2441..94db9ca92 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -73,6 +73,7 @@ buildscript { allprojects { repositories { google() + mavenLocal() mavenCentral() } diff --git a/playground/TODO.md b/playground/TODO.md new file mode 100644 index 000000000..45a499d83 --- /dev/null +++ b/playground/TODO.md @@ -0,0 +1,5 @@ +## Playground TODO + +- [ ] QuackColor playground +- [ ] QuackIcon playground +- [ ] IME animation playground (QuackLargeButton) diff --git a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ButtonPlayground.kt b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ButtonPlayground.kt index 5ad356fc3..8c4da43f8 100644 --- a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ButtonPlayground.kt +++ b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ButtonPlayground.kt @@ -24,6 +24,8 @@ import team.duckie.quackquack.ui.component.QuackSmallButtonType import team.duckie.quackquack.ui.component.QuackToggleChip import team.duckie.quackquack.ui.icon.QuackIcon +// TODO: IME 애니메이션 플레이그라운드 -> common 로직 변경하는 대공사 필요 + class ButtonPlayground : PlaygroundActivity( name = "Button", ) { diff --git a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ImagePlayground.kt b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ImagePlayground.kt index 535305e51..1152eb123 100644 --- a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ImagePlayground.kt +++ b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/ImagePlayground.kt @@ -7,6 +7,7 @@ package team.duckie.quackquack.playground.realworld +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -23,6 +24,7 @@ import team.duckie.quackquack.playground.base.PlaygroundActivity import team.duckie.quackquack.playground.util.rememberToast import team.duckie.quackquack.ui.component.QuackImage import team.duckie.quackquack.ui.component.QuackSelectableImage +import team.duckie.quackquack.ui.component.QuackSelectableImageType import team.duckie.quackquack.ui.util.DpSize class ImagePlayground : PlaygroundActivity( @@ -30,7 +32,8 @@ class ImagePlayground : PlaygroundActivity( ) { override val items: ImmutableList Unit>> = persistentListOf( ::QuackImageDemo.name to { QuackImageDemo() }, - ::QuackSelectableImageDemo.name to { QuackSelectableImageDemo() }, + ::QuackSelectableImageTypeTopEndCheckboxDemo.name to { QuackSelectableImageTypeTopEndCheckboxDemo() }, + ::QuackSelectableImageTypeCheckOverlayDemo.name to { QuackSelectableImageTypeCheckOverlayDemo() }, ) } @@ -57,11 +60,12 @@ fun QuackImageDemo() { ), rippleEnabled = false, onClick = { toast("QuackImage clicked") }, + onLongClick = { toast("QuackImage long clicked") }, ) } @Composable -fun QuackSelectableImageDemo() { +fun QuackSelectableImageTypeTopEndCheckboxDemo() { val context = LocalContext.current var selected by remember { mutableStateOf(false) } @@ -79,3 +83,25 @@ fun QuackSelectableImageDemo() { onClick = { selected = !selected }, ) } + +@Composable +fun QuackSelectableImageTypeCheckOverlayDemo() { + val context = LocalContext.current + var selected by remember { mutableStateOf(false) } + + QuackSelectableImage( + src = remember { + ContextCompat.getDrawable( + context, R.drawable.ic_quack, + ) + }, + size = DpSize( + all = 250.dp, + ), + shape = CircleShape, + isSelected = selected, + selectableType = QuackSelectableImageType.CheckOverlay, + rippleEnabled = false, + onClick = { selected = !selected }, + ) +} diff --git a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TagPlayground.kt b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TagPlayground.kt index 9106c8555..4644548ed 100644 --- a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TagPlayground.kt +++ b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TagPlayground.kt @@ -64,3 +64,6 @@ fun QuackRoundTagDemo() { onClick = { selected = !selected }, ) } + +// TODO: QuackLazyVerticalGridTag 플레이그라운드 +// QuackLazyVerticalGridTag 가 정말 꽥꽥에서 제공이 필요할까? diff --git a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TextFieldPlayground.kt b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TextFieldPlayground.kt index 3fe0723e4..79a91dcdf 100644 --- a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TextFieldPlayground.kt +++ b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TextFieldPlayground.kt @@ -8,6 +8,8 @@ package team.duckie.quackquack.playground.realworld import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import kotlinx.collections.immutable.ImmutableList @@ -16,8 +18,8 @@ import team.duckie.quackquack.playground.base.PlaygroundActivity import team.duckie.quackquack.playground.util.rememberToast import team.duckie.quackquack.ui.component.QuackBasic2TextField import team.duckie.quackquack.ui.component.QuackBasicTextField +import team.duckie.quackquack.ui.component.QuackErrorableTextField import team.duckie.quackquack.ui.component.QuackPriceTextField -import team.duckie.quackquack.ui.component.QuackProfileTextField import team.duckie.quackquack.ui.icon.QuackIcon class TextFieldPlayground : PlaygroundActivity( @@ -27,7 +29,8 @@ class TextFieldPlayground : PlaygroundActivity( ::QuackBasicTextFieldDemo.name to { QuackBasicTextFieldDemo() }, ::QuackPriceTextFieldDemo.name to { QuackPriceTextFieldDemo() }, ::QuackBasic2TextFieldDemo.name to { QuackBasic2TextFieldDemo() }, - ::QuackProfileTextFieldDemo.name to { QuackProfileTextFieldDemo() }, + ::QuackErrorableTextFieldDemo.name to { QuackErrorableTextFieldDemo() }, + ::QuackErrorableTextFieldWithoutClearButtonDemo.name to { QuackErrorableTextFieldWithoutClearButtonDemo() }, ) } @@ -74,15 +77,41 @@ fun QuackBasic2TextFieldDemo() { } @Composable -fun QuackProfileTextFieldDemo() { +fun QuackErrorableTextFieldDemo() { val (text, setText) = remember { mutableStateOf("") } + val isError by remember { + derivedStateOf { + text.length > 5 + } + } - QuackProfileTextField( + QuackErrorableTextField( text = text, onTextChanged = setText, placeholderText = "MaxLength: 5", maxLength = 5, + isError = isError, errorText = "ErrorText", + showClearButton = true, onCleared = { setText("") }, ) } + +@Composable +fun QuackErrorableTextFieldWithoutClearButtonDemo() { + val (text, setText) = remember { mutableStateOf("") } + val isError by remember { + derivedStateOf { + text.length > 5 + } + } + + QuackErrorableTextField( + text = text, + onTextChanged = setText, + placeholderText = "MaxLength: 5", + maxLength = 5, + isError = isError, + errorText = "ErrorText", + ) +} diff --git a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TopAppBarPlayground.kt b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TopAppBarPlayground.kt index ee59dda2f..6014f5a0f 100644 --- a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TopAppBarPlayground.kt +++ b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TopAppBarPlayground.kt @@ -21,6 +21,7 @@ class TopAppBarPlayground : PlaygroundActivity( override val items: ImmutableList Unit>> = persistentListOf( ::QuackTopAppBarTypeLogoAndIconsDemo.name to { QuackTopAppBarTypeLogoAndIconsDemo() }, ::QuackTopAppBarTypeTextDemo.name to { QuackTopAppBarTypeTextDemo() }, + ::QuackTopAppBarOnlyLeadingContentsDemo.name to { QuackTopAppBarOnlyLeadingContentsDemo() }, ) } @@ -50,8 +51,20 @@ fun QuackTopAppBarTypeTextDemo() { leadingText = "Heart", onLeadingIconClick = { toast("Heart clicked") }, centerText = "DUCKIE!", + centerTextTrailingIcon = QuackIcon.ArrowDown, onCenterClick = { toast("Logo clicked") }, trailingText = "trailing", onTrailingTextClick = { toast("trailing clicked") }, ) } + +@Composable +fun QuackTopAppBarOnlyLeadingContentsDemo() { + val toast = rememberToast() + + QuackTopAppBar( + leadingIcon = QuackIcon.Heart, + leadingText = "Heart", + onLeadingIconClick = { toast("Heart clicked") }, + ) +} diff --git a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TypographyPlayground.kt b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TypographyPlayground.kt index e93ce9bbd..986b5b789 100644 --- a/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TypographyPlayground.kt +++ b/playground/src/main/kotlin/team/duckie/quackquack/playground/realworld/TypographyPlayground.kt @@ -7,7 +7,9 @@ package team.duckie.quackquack.playground.realworld +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import team.duckie.quackquack.playground.base.PlaygroundActivity @@ -19,6 +21,7 @@ import team.duckie.quackquack.ui.component.QuackBody3 import team.duckie.quackquack.ui.component.QuackHeadLine1 import team.duckie.quackquack.ui.component.QuackHeadLine2 import team.duckie.quackquack.ui.component.QuackHighlightBody1 +import team.duckie.quackquack.ui.component.QuackSplashSlogan import team.duckie.quackquack.ui.component.QuackSubtitle import team.duckie.quackquack.ui.component.QuackSubtitle2 import team.duckie.quackquack.ui.component.QuackTitle1 @@ -30,6 +33,7 @@ class TypographyPlayground : PlaygroundActivity( name = "Typography", ) { override val items: ImmutableList Unit>> = persistentListOf( + ::QuackSplashSloganDemo.name to { QuackSplashSloganDemo() }, ::QuackHeadLine1Demo.name to { QuackHeadLine1Demo() }, ::QuackHeadLine2Demo.name to { QuackHeadLine2Demo() }, ::QuackTitle1Demo.name to { QuackTitle1Demo() }, @@ -46,6 +50,17 @@ class TypographyPlayground : PlaygroundActivity( ) } +@Composable +fun QuackSplashSloganDemo() { + val toast = rememberToast() + + QuackSplashSlogan( + text = "QuackSplashSlogan + 30.dp padding (for click area test)", + onClick = { toast("QuackSplashSlogan") }, + padding = PaddingValues(30.dp), + ) +} + @Composable fun QuackHeadLine1Demo() { val toast = rememberToast() diff --git a/settings.gradle.kts b/settings.gradle.kts index 86ad03ae6..20e4274dd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,6 +15,7 @@ pluginManagement { includeBuild("build-logic") repositories { google() + mavenLocal() mavenCentral() gradlePluginPortal() } diff --git a/ui-components/api/ui-components.api b/ui-components/api/ui-components.api index 22ba2e973..349becf8d 100644 --- a/ui-components/api/ui-components.api +++ b/ui-components/api/ui-components.api @@ -9,6 +9,10 @@ public final class team/duckie/quackquack/ui/animation/QuackAnimatedContentKt { public static final fun QuackAnimatedContent (Landroidx/compose/ui/Modifier;Ljava/lang/Object;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V } +public final class team/duckie/quackquack/ui/animation/QuackAnimatedVisibilityKt { + public static final fun QuackAnimatedVisibility (Landroidx/compose/ui/Modifier;ZLjava/lang/String;Landroidx/compose/animation/EnterTransition;Landroidx/compose/animation/ExitTransition;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;II)V +} + public final class team/duckie/quackquack/ui/animation/QuackAnimationSpec { public static final field $stable I public static final field INSTANCE Lteam/duckie/quackquack/ui/animation/QuackAnimationSpec; diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/animation/QuackAnimatedVisibility.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/animation/QuackAnimatedVisibility.kt new file mode 100644 index 000000000..8a5ab4e77 --- /dev/null +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/animation/QuackAnimatedVisibility.kt @@ -0,0 +1,63 @@ +/* + * Designed and developed by Duckie Team, 2022 + * + * Licensed under the MIT. + * Please see full license: https://github.com/duckie-team/duckie-quack-quack/blob/main/LICENSE + */ + +package team.duckie.quackquack.ui.animation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import team.duckie.quackquack.ui.util.runIf + +/** + * 컴포저블의 visiblilty 변화에 애니메이션을 적용하는 컨테이너 입니다. + * + * @param modifier 이 컨테이너에 적용할 [Modifier] + * @param visible visibility 여부 + * @param label 내부 구현인 TransitionAPI 에 사용될 label + * @param otherEnterAnimation 추가로 더할 enter 애니메이션 + * @param otherExitAnimation 추가로 더할 exit 애니메이션 + * @param content visiblilty 애니메이션이 적용될 컴포저블 컨텐츠 + */ +@Composable +public fun QuackAnimatedVisibility( + modifier: Modifier = Modifier, + visible: Boolean, + label: String = "AnimatedVisibility", + otherEnterAnimation: EnterTransition? = null, + otherExitAnimation: ExitTransition? = null, + content: @Composable AnimatedVisibilityScope.() -> Unit, +) { + AnimatedVisibility( + modifier = modifier, + visible = visible, + enter = fadeIn( + animationSpec = QuackAnimationSpec(), + ).runIf( + condition = otherEnterAnimation != null, + ) { + plus( + enter = otherEnterAnimation!!, + ) + }, + exit = fadeOut( + animationSpec = QuackAnimationSpec(), + ).runIf( + condition = otherExitAnimation != null, + ) { + plus( + exit = otherExitAnimation!!, + ) + }, + label = label, + content = content, + ) +} diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/color/QuackColor.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/color/QuackColor.kt index a3f3cbb40..e206b0ad2 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/color/QuackColor.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/color/QuackColor.kt @@ -105,6 +105,12 @@ public value class QuackColor internal constructor( ), ) + public val Dimmed: QuackColor = QuackColor( + composeColor = Color.Black.copy( + alpha = 0.6f, + ), + ) + public val Gray1: QuackColor = QuackColor( composeColor = Color( color = 0xFF666666, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/DropDown.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/DropDown.kt index e9db27f4f..a88d48c8c 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/DropDown.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/DropDown.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -78,7 +77,6 @@ public fun QuackDropDownCard( ) { Row( modifier = modifier - .wrapContentSize() .clip( shape = Shape, ) diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextArea.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextArea.kt index 347c48e2b..3abb1daa6 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextArea.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextArea.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -274,7 +273,6 @@ public fun QuackBasicTextArea( BasicTextField( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .background( color = BackgroundColor.composeColor, ) diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextField.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextField.kt index 9449a014d..37a1e469a 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextField.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TextField.kt @@ -10,10 +10,7 @@ package team.duckie.quackquack.ui.component import android.annotation.SuppressLint -import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -23,8 +20,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -46,7 +41,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.zIndex -import team.duckie.quackquack.ui.animation.QuackAnimatedContent +import team.duckie.quackquack.ui.animation.QuackAnimatedVisibility import team.duckie.quackquack.ui.animation.QuackAnimationSpec import team.duckie.quackquack.ui.color.QuackColor import team.duckie.quackquack.ui.component.internal.QuackText @@ -57,8 +52,10 @@ import team.duckie.quackquack.ui.modifier.quackClickable import team.duckie.quackquack.ui.textstyle.QuackTextStyle import team.duckie.quackquack.ui.theme.LocalQuackTextFieldColors import team.duckie.quackquack.ui.util.DpSize +import team.duckie.quackquack.ui.util.heightOrZero import team.duckie.quackquack.ui.util.npe import team.duckie.quackquack.ui.util.runIf +import team.duckie.quackquack.ui.util.runtimeCheck /** * QuackTextField 의 리소스 모음 @@ -240,7 +237,6 @@ private object QuackTextFieldDefaults { { QuackText( modifier = Modifier - .wrapContentSize() .quackClickable( rippleEnabled = false, onClick = onClick, @@ -420,7 +416,7 @@ private object QuackTextFieldDefaults { } } - object Profile { + object Errorable { val InputTextPadding = PaddingValues( top = 16.dp, bottom = 8.dp, @@ -591,7 +587,6 @@ private object QuackTextFieldDefaults { // QuackText X Text( modifier = Modifier - .wrapContentSize() .padding( paddingValues = ErrorTextPadding, ) @@ -619,6 +614,7 @@ private object QuackTextFieldDefaults { * * @param state 입력된 글자 수 * @param baseline 최대 입력 가능한 글자 수 + * @param showClearButton 글자 초기화 버튼을 표시할지 여부 * @param onCleared 글자 초기화 버튼을 눌렀을 때 호출될 콜백 * * @return trailing content 에 배치되는 컴포저블 @@ -628,7 +624,8 @@ private object QuackTextFieldDefaults { fun TrailingContent( state: Int, baseline: Int, - onCleared: () -> Unit, + showClearButton: Boolean, + onCleared: (() -> Unit)?, ): @Composable () -> Unit { // [요구 사항] // 기본적으로 profile text field 의 trailing content 는 2개로 나뉨 @@ -640,49 +637,42 @@ private object QuackTextFieldDefaults { // gone 하고 원래 가로 길이만큼 추가 start 패딩을 갖는 식으로 구현해야 함 return { - QuackAnimatedContent( - modifier = Modifier - .wrapContentSize() - .padding( - paddingValues = TrailingContentPadding, - ), - targetState = state == 0, - ) { isEmpty -> - Layout( - modifier = Modifier.wrapContentSize(), - content = { - // counter - Row( - modifier = Modifier - .wrapContentSize() - .layoutId( - layoutId = TrailingCounterLayoutId, - ), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy( - space = TrailingCountingTextGap, - ), - ) { - QuackText( - text = state.toString(), - style = trailingCountingStateTextTypographyFor( - isEmpty = state == 0, - ), - singleLine = true, - ) - QuackText( - text = "/", - style = TrailingCountingtBaselineTexTypograhy, - singleLine = true, - ) - QuackText( - text = baseline, - style = TrailingCountingtBaselineTexTypograhy, - singleLine = true, - ) - } - // TODO: 레이아웃을 안깨고 터치 영역을 늘릴 수 있는 방법을 모르겠다 ㅠㅠ - // clear button + // counter, counter 는 전체 애니메이션 X + Layout( + modifier = Modifier.padding( + paddingValues = TrailingContentPadding, + ), + content = { + Row( + modifier = Modifier.layoutId( + layoutId = TrailingCounterLayoutId, + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy( + space = TrailingCountingTextGap, + ), + ) { + Text( + text = state.toString(), + style = trailingCountingStateTextTypographyFor( + isEmpty = state == 0, + ).asComposeStyle(), + maxLines = 1, + ) + Text( + text = "/", + style = TrailingCountingtBaselineTexTypograhy.asComposeStyle(), + maxLines = 1, + ) + Text( + text = baseline.toString(), + style = TrailingCountingtBaselineTexTypograhy.asComposeStyle(), + maxLines = 1, + ) + } + // TODO: 레이아웃을 안깨고 터치 영역을 늘릴 수 있는 방법을 모르겠다 ㅠㅠ + // clear button + if (showClearButton) { QuackImage( modifier = Modifier.layoutId( layoutId = TrailingClearButtonLayoutId, @@ -691,66 +681,73 @@ private object QuackTextFieldDefaults { size = TrailingIconSize, tint = TrailingIconTint, rippleEnabled = false, - onClick = onCleared, + onClick = onCleared!!, // must non-null ) - }, - ) { measurables, constraints -> - val trailingCounterMeasurable = measurables.fastFirstOrNull { measurable -> - measurable.layoutId == TrailingCounterLayoutId - } ?: npe() - val trailingClearButtonMeasurable = measurables.fastFirstOrNull { measurable -> - measurable.layoutId == TrailingClearButtonLayoutId - } ?: npe() - - val trailingCounterPlaceable = trailingCounterMeasurable.measure( - constraints = constraints, - ) - val trailingClearButtonPlaceable = trailingClearButtonMeasurable.measure( - constraints = constraints, - ) + } + }, + ) { measurables, constraints -> + val trailingCounterMeasurable = measurables.fastFirstOrNull { measurable -> + measurable.layoutId == TrailingCounterLayoutId + } ?: npe() + val trailingClearButtonMeasurable = measurables.fastFirstOrNull { measurable -> + measurable.layoutId == TrailingClearButtonLayoutId + } - val maxWidth = trailingCounterPlaceable.width - .plus( - other = trailingClearButtonPlaceable.width, - ) - .plus( - other = TrailingContentGap.roundToPx(), - ) - val maxHeight = maxOf( - a = trailingCounterPlaceable.height, - b = trailingClearButtonPlaceable.height, - ) + val trailingCounterPlaceable = trailingCounterMeasurable.measure( + constraints = constraints, + ) + val trailingClearButtonPlaceable = trailingClearButtonMeasurable?.measure( + constraints = constraints, + ) - layout( - width = maxWidth, - height = maxHeight, + val maxWidth = trailingCounterPlaceable.width + .runIf( + condition = trailingClearButtonPlaceable != null, ) { - when (isEmpty) { - true -> { // counter 만 표시 - trailingCounterPlaceable.place( - x = trailingClearButtonPlaceable.width + TrailingContentGap.roundToPx(), - y = Alignment.CenterVertically.align( - size = trailingCounterPlaceable.height, - space = maxHeight, - ), - ) - } - else -> { // counter, clear button 모두 표시 - trailingCounterPlaceable.place( - x = 0, - y = Alignment.CenterVertically.align( - size = trailingCounterPlaceable.height, - space = maxHeight, - ), - ) - trailingClearButtonPlaceable.place( - x = trailingCounterPlaceable.width + TrailingContentGap.roundToPx(), - y = Alignment.CenterVertically.align( - size = trailingClearButtonPlaceable.height, - space = maxHeight, - ), - ) - } + this + .plus( + other = trailingClearButtonPlaceable!!.width, + ) + .plus( + other = TrailingContentGap.roundToPx(), + ) + } + val maxHeight = maxOf( + a = trailingCounterPlaceable.height, + b = trailingClearButtonPlaceable.heightOrZero(), + ) + + layout( + width = maxWidth, + height = maxHeight, + ) { + when (state == 0) { // isEmpty + true -> { // counter 만 표시 + trailingCounterPlaceable.place( + x = trailingClearButtonPlaceable?.width?.plus( + other = TrailingContentGap.roundToPx(), + ) ?: 0, + y = Alignment.CenterVertically.align( + size = trailingCounterPlaceable.height, + space = maxHeight, + ), + ) + } + else -> { // counter, clear button 모두 표시 + trailingCounterPlaceable.place( + x = 0, + y = Alignment.CenterVertically.align( + size = trailingCounterPlaceable.height, + space = maxHeight, + ), + ) + trailingClearButtonPlaceable?.place( + x = trailingCounterPlaceable.width + TrailingContentGap.roundToPx(), + y = Alignment.CenterVertically.align( + size = trailingClearButtonPlaceable.height, + space = maxHeight, + ), + ) } } } @@ -871,7 +868,6 @@ public fun QuackBasicTextField( BasicTextField( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .drawAnimatedLine( thickness = UnderlineHeight, color = UnderlineColor, @@ -971,7 +967,6 @@ public fun QuackPriceTextField( BasicTextField( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .drawAnimatedLine( thickness = UnderlineHeight, color = UnderlineColor, @@ -1081,7 +1076,6 @@ public fun QuackBasic2TextField( BasicTextField( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .drawAnimatedLine( thickness = UnderlineHeight, color = UnderlineColor, @@ -1137,14 +1131,13 @@ public fun QuackBasic2TextField( /** * 닉네임 입력에 사용되는 TextField 를 그립니다. - * [QuackProfileTextField] 는 크게 다음과 같은 특징을 갖습니다. + * [QuackErrorableTextField] 는 크게 다음과 같은 특징을 갖습니다. * * 1. underline 이 컴포넌트 하단에 표시됩니다. - * 2. 항상 [QuackIcon.Close] 을 trailing icon 으로 표시합니다. + * 2. 선택적으로 [QuackIcon.Close] 을 trailing icon 으로 표시합니다. * 3. 입력된 텍스트의 Counter 를 trailing text 로 항상 표시합니다. * 4. 항상 [KeyboardType.Text] 타입의 키보드를 사용합니다. - * 5. 입력 가능한 최대 글자 수를 받습니다. - * 만약 이 수를 넘어섰다면 에러 텍스트를 표시합니다. + * 5. 입력 가능한 최대 글자 수를 받습니다. 만약 이 수를 넘어섰다면 에러 텍스트를 표시합니다. * 6. 항상 상위 컴포저블의 가로 길이에 꽉차게 그려집니다. * * @param modifier 이 컴포넌트에 적용할 [Modifier] @@ -1152,31 +1145,40 @@ public fun QuackBasic2TextField( * @param text 표시할 텍스트 * @param onTextChanged 새로운 텍스트가 입력됐을 때 호출될 람다 * @param placeholderText 텍스트가 입력되지 않았을 때 표시할 텍스트 + * @param isError 현재 에러 상태에 있는지 여부 * @param errorText 최대 입력 가능한 글자 수를 넘었을 때 표시할 에러 텍스트 - * @param onCleared trailing content 가 클릭됐을 때 호출될 람다 + * @param showClearButton trailing content 에 clear icon 을 배치할지 여부 + * @param onCleared clear icon 이 클릭됐을 때 호출될 람다 * @param imeAction 키보드 옵션 * @param keyboardActions 키보드 액션 */ @Composable -public fun QuackProfileTextField( +public fun QuackErrorableTextField( modifier: Modifier = Modifier, text: String, onTextChanged: (text: String) -> Unit, placeholderText: String, maxLength: Int, + isError: Boolean, errorText: String, - onCleared: () -> Unit, + showClearButton: Boolean = false, + onCleared: (() -> Unit)? = null, imeAction: ImeAction = ImeAction.Done, keyboardActions: KeyboardActions = KeyboardActions(), ): Unit = with( - receiver = QuackTextFieldDefaults.Profile, + receiver = QuackTextFieldDefaults.Errorable, ) { + if (showClearButton) { + runtimeCheck(onCleared != null) { + "onCleared must not be null when showClearButton is true" + } + } + val quackTextFieldColors = LocalQuackTextFieldColors.current // 리컴포지션이 되는 메인 조건은 Text 가 바뀌었을 때인데 그러면 // 어차피 항상 재계산 되므로 굳이 remember 를 할 필요가 없음 val isPlaceholder = text.isEmpty() - val isError = text.length > maxLength // 애니메이션 적용 X val inputTypography = remember( @@ -1188,12 +1190,11 @@ public fun QuackProfileTextField( } Column( - modifier = modifier.wrapContentSize(), + modifier = modifier, ) { BasicTextField( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .drawAnimatedLine( thickness = UnderlineHeight, color = underlineColorFor( @@ -1241,31 +1242,26 @@ public fun QuackProfileTextField( trailingContent = TrailingContent( state = text.length, baseline = maxLength, + showClearButton = showClearButton, onCleared = onCleared, ), ) }, ) - Box( - modifier = Modifier.wrapContentSize(), - ) { + Box { ErrorText( text = errorText, visible = false, ) - this@Column.AnimatedVisibility( + QuackAnimatedVisibility( visible = isError, modifier = Modifier.zIndex( zIndex = 2f, ), - enter = fadeIn( - animationSpec = QuackAnimationSpec(), - ) + expandVertically( + otherEnterAnimation = expandVertically( animationSpec = QuackAnimationSpec(), ), - exit = fadeOut( - animationSpec = QuackAnimationSpec(), - ) + shrinkVertically( + otherExitAnimation = shrinkVertically( animationSpec = QuackAnimationSpec(), ), ) { diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TopAppBar.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TopAppBar.kt index 6397800ec..ec065b4fb 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TopAppBar.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/TopAppBar.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Alignment @@ -57,7 +56,6 @@ private object QuackTopAppBarDefaults { private val CenterTextPadding = PaddingValues( vertical = 15.dp, ) - private val CenterTrailingIcon = QuackIcon.ArrowDown private val CenterIconTint = QuackColor.Gray1 /** @@ -113,7 +111,6 @@ private object QuackTopAppBarDefaults { onIconClick: () -> Unit, ) { Row( - modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically, ) { QuackImage( @@ -141,23 +138,20 @@ private object QuackTopAppBarDefaults { * - [showLogo] 이 true 라면 [text] 값은 무시됩니다. * 로고 리소스로는 [QuackIcon.TextLogo] 를 사용합니다. * - [text] 값이 있다면 [showLogo] 값은 무시됩니다. - * 또한 [text] 는 항상 trailing icon 으로 [QuackIcon.ArrowDown] 을 갖습니다. + * 또한 [text] 는 trailing icon 을 가질 수 있습니다. * * @param showLogo 덕키의 로고를 배치할지 여부 * @param text 로고 대신에 표시할 텍스트 + * @param textTrailingIcon [text] 의 trailing content 로 표시될 아이콘 * @param onClick center content 가 클릭됐을 때 실행될 람다 */ @Composable fun CenterContent( showLogo: Boolean? = null, text: String? = null, + textTrailingIcon: QuackIcon? = null, onClick: (() -> Unit)? = null, ) { - runtimeCheck( - value = showLogo != null || text != null, - ) { - "logoType or text must be not null" - } if (showLogo == true) { runtimeCheck( value = text == null, @@ -166,12 +160,10 @@ private object QuackTopAppBarDefaults { } } Row( - modifier = Modifier - .wrapContentSize() - .quackClickable( - rippleEnabled = false, - onClick = onClick, - ), + modifier = Modifier.quackClickable( + rippleEnabled = false, + onClick = onClick, + ), verticalAlignment = Alignment.CenterVertically, ) { if (showLogo == true) { @@ -191,7 +183,7 @@ private object QuackTopAppBarDefaults { singleLine = true, ) QuackImage( - src = CenterTrailingIcon, + src = textTrailingIcon, size = IconSize, tint = CenterIconTint, ) @@ -223,11 +215,6 @@ private object QuackTopAppBarDefaults { onExtraIconClick: (() -> Unit)? = null, onTextClick: (() -> Unit)? = null, ) { - runtimeCheck( - value = icon != null || extraIcon != null || text != null, - ) { - "icon or extraIcon or text must be not null" - } if (icon != null || extraIcon != null) { runtimeCheck( value = text == null, @@ -243,7 +230,6 @@ private object QuackTopAppBarDefaults { } } Row( - modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically, ) { text?.let { @@ -295,7 +281,7 @@ private object QuackTopAppBarDefaults { * - [showLogoAtCenter] 이 true 라면 [centerText] 값은 무시됩니다. * 로고 리소스로는 [QuackIcon.TextLogo] 를 사용합니다. * - [centerText] 값이 있다면 [showLogoAtCenter] 값은 무시됩니다. - * 또한 [centerText] 는 항상 trailing icon 으로 [QuackIcon.ArrowDown] 을 갖습니다. + * 또한 [centerText] 는 trailing content 로 아이콘을 배치할 수 있습니다. * - [trailingExtraIcon] 과 [trailingIcon] 이 하나라도 들어왔다면 [trailingText] 는 무시되며, * `[trailingExtraIcon] [trailingIcon]` 순서로 배치됩니다. * - [trailingText] 값이 입력되면 [trailingExtraIcon] 과 [trailingIcon] 값은 무시됩니다. @@ -306,6 +292,7 @@ private object QuackTopAppBarDefaults { * @param onLeadingIconClick [leadingIcon] 이 클릭됐을 때 실행될 람다 * @param showLogoAtCenter center content 로 덕키의 로고를 배치할지 여부 * @param centerText center content 에 로고 대신에 표시할 텍스트 + * @param centerTextTrailingIcon [centerText] 의 trailing content 로 배치할 아이콘 * @param onCenterClick center content 가 클릭됐을 때 실행될 람다 * @param trailingIcon 배치할 아이콘 * @param trailingExtraIcon 추가로 배치할 아이콘 @@ -322,6 +309,7 @@ public fun QuackTopAppBar( onLeadingIconClick: () -> Unit, showLogoAtCenter: Boolean? = null, centerText: String? = null, + centerTextTrailingIcon: QuackIcon? = null, onCenterClick: (() -> Unit)? = null, trailingIcon: QuackIcon? = null, trailingExtraIcon: QuackIcon? = null, @@ -349,6 +337,7 @@ public fun QuackTopAppBar( CenterContent( showLogo = showLogoAtCenter, text = centerText, + textTrailingIcon = centerTextTrailingIcon, onClick = onCenterClick, ) TrailingContent( diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/button.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/button.kt index 162a792a8..86fa9658a 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/button.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/button.kt @@ -16,10 +16,12 @@ import android.annotation.SuppressLint import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.ime +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -30,6 +32,7 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import team.duckie.quackquack.ui.border.QuackBorder @@ -412,6 +415,10 @@ private object QuackButtonDefaults { * 2. 자동으로 모든 영역에 애니메이션이 적용됩니다. (IME 포함) * 3. 항상 상위 컴포저블의 가로 길이에 꽉차게 그려집니다. * + * - IME 애니메이션을 적용하기 위해선 해당 액티비티의 `windowSoftInputMode` 가 + * `adjustResize` 로 설정되어 있어야 합니다. + * ` + * * @param modifier 이 컴포넌트에 적용할 [Modifier] * @param type 이 버튼의 사용 사례에 적합한 버튼 타입 * @param text 버튼에 표시될 텍스트 @@ -451,10 +458,21 @@ public fun QuackLargeButton( } } + val imeInsets = WindowInsets.ime + val navigationBarInsets = WindowInsets.navigationBars + QuackBasicButton( modifier = modifier .fillMaxWidth() - .wrapContentHeight(), + .offset { + val imeHeight = imeInsets.getBottom(this) + val nagivationBarHeight = navigationBarInsets.getBottom(this) + // ime height 에 navigation height 가 포함되는 것으로 추측됨 + val yOffset = imeHeight + .minus(nagivationBarHeight) + .coerceAtLeast(0) + IntOffset(x = 0, y = -yOffset) + }, shape = Shape, leadingContent = LeadingContent( leadingIcon = leadingIcon, @@ -474,6 +492,7 @@ public fun QuackLargeButton( type = type, ), onClick = onClick, + rippleEnabled = enabled ?: true, ) } @@ -499,7 +518,7 @@ public fun QuackMediumToggleButton( receiver = QuackButtonDefaults.MediumButton, ) { QuackBasicButton( - modifier = modifier.wrapContentSize(), + modifier = modifier, shape = Shape, text = text, textStyle = typographyFor( @@ -539,7 +558,7 @@ public fun QuackSmallButton( receiver = QuackButtonDefaults.SmallButton, ) { QuackBasicButton( - modifier = modifier.wrapContentSize(), + modifier = modifier, shape = Shape, text = text, textStyle = typographyFor( @@ -584,7 +603,7 @@ public fun QuackToggleChip( receiver = QuackButtonDefaults.ChipButton, ) { QuackBasicButton( - modifier = modifier.wrapContentSize(), + modifier = modifier, shape = Shape, text = text, textStyle = typographyFor( @@ -651,11 +670,9 @@ private fun QuackBasicButton( onClick = onClick, ) { Row( - modifier = Modifier - .wrapContentSize() - .padding( - paddingValues = padding, - ), + modifier = Modifier.padding( + paddingValues = padding, + ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy( space = 2.dp, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/image.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/image.kt index 989ebdca3..64a3e11ae 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/image.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/image.kt @@ -9,9 +9,9 @@ package team.duckie.quackquack.ui.component import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -35,13 +35,18 @@ import androidx.compose.ui.zIndex import coil.compose.AsyncImage import coil.request.ImageRequest import team.duckie.quackquack.ui.animation.QuackAnimatedContent +import team.duckie.quackquack.ui.animation.QuackAnimatedVisibility import team.duckie.quackquack.ui.border.QuackBorder import team.duckie.quackquack.ui.color.QuackColor import team.duckie.quackquack.ui.color.animateQuackColorAsState +import team.duckie.quackquack.ui.component.QuackSelectableImageType.CheckOverlay +import team.duckie.quackquack.ui.component.QuackSelectableImageType.TopEndCheckBox import team.duckie.quackquack.ui.component.internal.QuackSurface import team.duckie.quackquack.ui.icon.QuackIcon import team.duckie.quackquack.ui.modifier.quackClickable +import team.duckie.quackquack.ui.util.DpSize import team.duckie.quackquack.ui.util.runIf +import team.duckie.quackquack.ui.util.runtimeCheck /** * QuackSelectableImage 를 그리는데 필요한 리소스를 구성합니다. @@ -81,7 +86,7 @@ private object QuackImageDefaults { @Suppress("unused") object Deletion { val Icon = QuackIcon.Delete - val IconSize = team.duckie.quackquack.ui.util.DpSize( + val IconSize = DpSize( all = 10.dp, ) @@ -89,7 +94,7 @@ private object QuackImageDefaults { val IconContainerBackgroundColor = QuackColor.Black.change( alpha = 0.5f ) - val IconContainerSize = team.duckie.quackquack.ui.util.DpSize( + val IconContainerSize = DpSize( all = 16.dp, ) } @@ -105,6 +110,8 @@ private object QuackImageDefaults { * @param tint 적용할 틴트 값 * @param rippleEnabled 클릭됐을 때 ripple 발생 여부 * @param onClick 클릭됐을 때 실행할 람다식 + * @param onLongClick 길게 클릭됐을 때 실행할 람다식. + * 이 값이 제공되면 [onClick] 값이 필수로 제공돼야 합니다. * @param contentScale 적용할 content scale 정책 * @param shape 리소스가 표시될 모양 * @param badge 리소스와 함께 표시할 배지 @@ -121,37 +128,49 @@ public fun QuackImage( tint: QuackColor? = null, rippleEnabled: Boolean = true, onClick: (() -> Unit)? = null, + onLongClick: (() -> Unit)? = null, contentScale: ContentScale = ContentScale.FillBounds, shape: Shape = RectangleShape, badge: (@Composable () -> Unit)? = null, badgeSize: DpSize? = null, badgeAlign: Alignment = Alignment.TopEnd, contentDescription: String? = null, -): Unit = QuackImageInternal( - modifier = modifier - .clip( - shape = shape, - ) - .quackClickable( - rippleEnabled = rippleEnabled, - onClick = onClick, - ) - .runIf( - condition = padding != null, +) { + if (onLongClick != null) { + runtimeCheck( + value = onClick != null, ) { - padding( - paddingValues = padding!!, + "onLongClick 값이 제공되면 onClick 값이 필수로 제공돼야 합니다." + } + } + + QuackImageInternal( + modifier = modifier + .clip( + shape = shape, ) - }, - src = src, - size = size, - tint = tint, - contentScale = contentScale, - badge = badge, - badgeSize = badgeSize, - badgeAlign = badgeAlign, - contentDescription = contentDescription, -) + .quackClickable( + rippleEnabled = rippleEnabled, + onClick = onClick, + onLongClick = onLongClick, + ) + .runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, + src = src, + size = size, + tint = tint, + contentScale = contentScale, + badge = badge, + badgeSize = badgeSize, + badgeAlign = badgeAlign, + contentDescription = contentDescription, + ) +} /** * [QuackImage] 를 실제로 그립니다 @@ -193,7 +212,7 @@ private fun QuackImageInternal( key2 = density, ) { when (size) { - null -> Modifier.wrapContentSize() + null -> Modifier else -> Modifier.size( size = size * density.fontScale, ) @@ -217,13 +236,12 @@ private fun QuackImageInternal( .padding( paddingValues = Margin, ) - .run { - when (badgeSize) { - null -> wrapContentSize() - else -> size( - size = badgeSize * density.fontScale, - ) - } + .runIf( + condition = badgeSize != null, + ) { + size( + size = badgeSize!! * density.fontScale, + ) } .zIndex( zIndex = 2f, @@ -261,7 +279,6 @@ private fun QuackImageInternal( modifier = containerModifier, ) { imageModel -> Box( - modifier = Modifier.wrapContentSize(), contentAlignment = badgeAlign, ) { AsyncImage( @@ -322,6 +339,27 @@ private fun QuackColor.toColorFilter(): ColorFilter? { } } +/** + * [QuackSelectableImage] 에서 selection 이 표시될 방식을 나타냅니다. + * + * @property TopEndCheckBox 오른쪽 상단에 [QuackRoundCheckBox] 로 표시 + * @property CheckOverlay 이미지 전체에 [QuackIcon.Check] 로 오버레이 표시 및 + * [QuackColor.Dimmed] 로 dimmed 처리 + * + * @param size 만약 [CheckOverlay] 방식일 때 [QuackIcon.Check] 의 사이즈 + * @param tint 만약 [CheckOverlay] 방식일 때 [QuackIcon.Check] 의 틴트 + */ +public sealed class QuackSelectableImageType( + internal val size: DpSize? = null, + internal val tint: QuackColor? = null, +) { + public object TopEndCheckBox : QuackSelectableImageType() + public object CheckOverlay : QuackSelectableImageType( + size = DpSize(all = 28.dp), + tint = QuackColor.White, + ) +} + /** * 오른쪽 상단에 체크박스와 함께 이미지 혹은 [QuackIcon] 을 표시합니다. * @@ -330,6 +368,8 @@ private fun QuackColor.toColorFilter(): ColorFilter? { * @param src 표시할 리소스. 만약 null 이 들어온다면 리소스를 그리지 않습니다. * @param size 리소스의 크기를 지정합니다. null 이 들어오면 기본 크기로 표시합니다. * @param tint 적용할 틴트 값 + * @param shape 컴포넌트의 모양 + * @param selectableType selection 이 표시될 방식 * @param rippleEnabled 클릭됐을 때 ripple 발생 여부 * @param onClick 클릭됐을 때 실행할 람다식 * @param contentScale 적용할 content scale 정책 @@ -342,6 +382,8 @@ public fun QuackSelectableImage( src: Any?, size: DpSize? = null, tint: QuackColor? = null, + shape: Shape = QuackImageDefaults.SelectableImage.Shape, + selectableType: QuackSelectableImageType = TopEndCheckBox, rippleEnabled: Boolean = true, onClick: (() -> Unit)? = null, contentScale: ContentScale = ContentScale.FillBounds, @@ -350,27 +392,53 @@ public fun QuackSelectableImage( receiver = QuackImageDefaults.SelectableImage, ) { QuackSurface( - modifier = modifier.wrapContentSize(), - shape = Shape, + modifier = modifier, + shape = shape, border = borderFor( isSelected = isSelected, ), - contentAlignment = Alignment.TopEnd, rippleEnabled = rippleEnabled, onClick = onClick, + contentAlignment = Alignment.TopEnd, ) { - QuackRoundCheckBox( - modifier = Modifier.padding( - paddingValues = Margin, - ), - checked = isSelected, - ) QuackImage( + modifier = Modifier.zIndex(1f), src = src, size = size, tint = tint, contentScale = contentScale, contentDescription = contentDescription, ) + when (selectableType) { + TopEndCheckBox -> { + QuackRoundCheckBox( + modifier = Modifier + .padding( + paddingValues = Margin, + ) + .zIndex(2f), + checked = isSelected, + ) + } + CheckOverlay -> { + QuackAnimatedVisibility( + modifier = Modifier + .matchParentSize() + .zIndex(2f), + visible = isSelected, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + QuackImage( + src = QuackIcon.Check, + size = selectableType.size!!, + tint = selectableType.tint!!, + ) + } + } + } + } } } diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/QuackText.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/QuackText.kt index 0a044fdba..c9aa812c3 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/QuackText.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/QuackText.kt @@ -17,13 +17,16 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.util.fastForEach import kotlinx.collections.immutable.ImmutableList import team.duckie.quackquack.ui.animation.QuackAnimatedContent -import team.duckie.quackquack.ui.component.HighlightTextInfo +import team.duckie.quackquack.ui.component.QuackHighlightTextInfo import team.duckie.quackquack.ui.textstyle.QuackTextStyle import team.duckie.quackquack.ui.textstyle.animatedQuackTextStyleAsState // TODO: overlay-ux-writing // public val quackTexts: SnapshotStateList = mutableStateListOf() +// NOTE: 스타일 가이드에 없는 typography 로 텍스트 표시가 필요할 때가 있어서 +// public 접근제한자로 변경함 + /** * 주어진 조건에 따라 텍스트를 표시합니다. * 꽥꽥에서 텍스트를 표시하는데 사용되는 최하위 컴포넌트 입니다. @@ -40,7 +43,7 @@ import team.duckie.quackquack.ui.textstyle.animatedQuackTextStyleAsState * @param overflow 최대 표시 가능 범위를 넘었을 때 텍스트를 처리할 정책 */ @Composable -internal fun QuackText( +public fun QuackText( modifier: Modifier = Modifier, text: Any, style: QuackTextStyle, @@ -83,7 +86,7 @@ internal fun QuackText( * @param overflow 최대 표시 가능 범위를 넘었을 때 텍스트를 처리할 정책 */ @Composable -internal fun QuackText( +public fun QuackText( modifier: Modifier = Modifier, annotatedText: AnnotatedString, style: QuackTextStyle, @@ -122,10 +125,10 @@ internal fun QuackText( * @param overflow 최대 표시 가능 범위를 넘었을 때 텍스트를 처리할 정책 */ @Composable -internal fun QuackClickableText( +public fun QuackClickableText( modifier: Modifier = Modifier, text: AnnotatedString, - clickEventTextInfo: ImmutableList, + clickEventTextInfo: ImmutableList, defaultOnClick: (() -> Unit)? = null, style: QuackTextStyle, singleLine: Boolean = false, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/dimmed.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/dimmed.kt index 8345a01fe..cb6578e64 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/dimmed.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/internal/dimmed.kt @@ -12,17 +12,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.zIndex - -/** - * QuackDimmed 를 그리는데 필요한 리소스들을 정의합니다. - */ -private object QuackDimmedDefauts { - val BackgroundColor = Color.Black.copy( - alpha = 0.6f, - ) -} +import team.duckie.quackquack.ui.color.QuackColor /** * Quack 에서 사용될 배경 dimmed 를 구현합니다. @@ -38,15 +29,13 @@ private object QuackDimmedDefauts { internal fun QuackBackgroundDimmed( zIndex: Float = 0f, enabled: Boolean, -) = with( - receiver = QuackDimmedDefauts, ) { if (enabled) { Box( modifier = Modifier .fillMaxSize() .background( - color = BackgroundColor, + color = QuackColor.Dimmed.composeColor, ) .zIndex( zIndex = zIndex, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/label.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/label.kt index 96b19f0ee..a90809157 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/label.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/label.kt @@ -9,7 +9,6 @@ package team.duckie.quackquack.ui.component import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable @@ -98,7 +97,7 @@ public fun QuackLabel( receiver = QuackLabelDefaults, ) { QuackSurface( - modifier = modifier.wrapContentSize(), + modifier = modifier, shape = Shape, backgroundColor = backgroundColorFor( isActive = active, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tab.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tab.kt index f41277ef5..fcd3e768d 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tab.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tab.kt @@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable @@ -148,7 +146,6 @@ private object QuackTabDefaults { ) { index, title -> QuackText( modifier = Modifier - .wrapContentSize() .quackClickable( rippleEnabled = false, ) { @@ -287,7 +284,6 @@ private object QuackTabDefaults { weight = 1f, ) .fillMaxWidth() - .wrapContentHeight() .onGloballyPositioned { layoutCoordinates -> onEachTabPositioned( /* index = */ @@ -416,7 +412,6 @@ public fun QuackMainTab( TabTextLazyRow( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .background( color = BackgroundColor.composeColor, ) @@ -557,7 +552,6 @@ public fun QuackSubTab( TabTextRow( modifier = modifier .fillMaxWidth() - .wrapContentHeight() .background( color = BackgroundColor.composeColor, ) diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tag.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tag.kt index 509546d9c..d01192047 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tag.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/tag.kt @@ -5,8 +5,11 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE */ +@file:OptIn(ExperimentalFoundationApi::class) + package team.duckie.quackquack.ui.component +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues @@ -14,8 +17,6 @@ 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.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.itemsIndexed @@ -27,9 +28,11 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import kotlinx.collections.immutable.ImmutableList +import team.duckie.quackquack.ui.animation.QuackAnimationSpec import team.duckie.quackquack.ui.border.QuackBorder import team.duckie.quackquack.ui.color.QuackColor import team.duckie.quackquack.ui.component.internal.QuackSurface @@ -37,6 +40,7 @@ import team.duckie.quackquack.ui.component.internal.QuackText import team.duckie.quackquack.ui.icon.QuackIcon import team.duckie.quackquack.ui.textstyle.QuackTextStyle import team.duckie.quackquack.ui.util.DpSize +import team.duckie.quackquack.ui.util.NoPadding import team.duckie.quackquack.ui.util.runIf import team.duckie.quackquack.ui.util.runtimeCheck @@ -321,7 +325,7 @@ private fun QuackGrayscaleTagInternal( onClickWithIndex = onClickWithIndex, ) QuackSurface( - modifier = modifier.wrapContentSize(), + modifier = modifier, backgroundColor = BackgroundColor, shape = Shape, onClick = onClick ?: onClickWithIndex?.let { @@ -334,7 +338,6 @@ private fun QuackGrayscaleTagInternal( }, ) { Row( - modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically, ) { QuackText( @@ -426,7 +429,7 @@ private fun QuackCircleTagInternal( val hasTrailingIcon = trailingIcon != null QuackSurface( - modifier = modifier.wrapContentSize(), + modifier = modifier, backgroundColor = backgroundColorFor( isSelected = isSelected, hasTrailingIcon = hasTrailingIcon, @@ -445,7 +448,6 @@ private fun QuackCircleTagInternal( }, ) { Row( - modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically, ) { QuackText( @@ -541,7 +543,7 @@ private fun QuackRoundTagInternal( onClickWithIndex = onClickWithIndex, ) QuackSurface( - modifier = modifier.wrapContentSize(), + modifier = modifier, backgroundColor = BackgroundColor, shape = Shape, border = borderFor( @@ -605,7 +607,7 @@ private fun quackTagInternalAssert( /** * [LazyVerticalGrid] 형식으로 주어진 태그들을 배치합니다. * 이 컴포넌트는 항상 상위 컴포저블의 가로 길이만큼 width 가 지정되고, - * (즉, 좌우 패딩이 허용되지 않습니다) 한 줄에 최대 2개가 들어갈 수 있습니다. + * (즉, 좌우 패딩이 허용되지 않습니다) 한 줄에 최대 [itemChunkedSize]개가 들어갈 수 있습니다. * 또한 가로와 세로 스크롤을 모두 지원합니다. * * 퍼포먼스 측면에서 [LazyLayout] 를 사용하는 것이 좋지만, 덕키의 경우 @@ -614,34 +616,47 @@ private fun quackTagInternalAssert( * [Row] + [Modifier.horizontalScroll] 를 사용하여 구현하였습니다. * * @param modifier 이 컴포넌트에 적용할 [Modifier] + * @param contentPadding 이 컴포넌트의 광역에 적용될 [PaddingValues] * @param title 상단에 표시될 제목. 만약 null 을 제공할 시 표시되지 않습니다. * @param items 표시할 태그들의 제목. **중복되는 태그 제목은 허용하지 않습니다.** * 이 항목은 바뀔 수 있으므로 [ImmutableList] 가 아닌 일반 [List] 로 받습니다. * @param itemSelections 태그들의 선택 여부. * 이 항목은 바뀔 수 있으므로 [ImmutableList] 가 아닌 [List] 로 받습니다. + * @param itemChunkedSize 한 칸에 들어갈 최대 아이템의 개수 + * @param horizontalSpace 아이템들의 가로 간격 + * @param verticalSpace 아이템들의 세로 간격 * @param tagType [QuackLazyVerticalGridTag] 에서 표시할 태그의 타입을 지정합니다. * 여러 종류의 태그가 [QuackLazyVerticalGridTag] 으로 표시될 수 있게 태그의 타입을 따로 받습니다. + * @param key a factory of stable and unique keys representing the item. Using the same key + * for multiple items in the list is not allowed. Type of the key should be saveable + * via Bundle on Android. If null is passed the position in the list will represent the key. + * When you specify the key the scroll position will be maintained based on the key, which + * means if you add/remove items before the current visible item the item with the given key + * will be kept as the first visible one. * @param onClick 사용자가 태그를 클릭했을 때 호출되는 람다. * 람다식의 인자로는 선택된 태그의 index 가 들어옵니다. */ @Composable public fun QuackLazyVerticalGridTag( modifier: Modifier = Modifier, + contentPadding: PaddingValues = NoPadding, title: String? = null, items: List, itemSelections: List? = null, + itemChunkedSize: Int, + horizontalSpace: Dp = QuackTagDefaults.LazyTag.HorizontalSpacedBy, + verticalSpace: Dp = QuackTagDefaults.LazyTag.VerticalSpacedBy, tagType: QuackTagType, + key: (( + index: Int, + items: List, + ) -> Any)? = null, onClick: ( index: Int, ) -> Unit, ): Unit = with( receiver = QuackTagDefaults.LazyTag, ) { - runtimeCheck( - value = items.toSet().size == items.size, - ) { - "Duplicate tag titles are not allowed." - } if (itemSelections != null) { runtimeCheck( value = items.size == itemSelections.size, @@ -654,15 +669,14 @@ public fun QuackLazyVerticalGridTag( key1 = items, ) { items.chunked( - size = 2, + size = itemChunkedSize, ) } LazyColumn( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight(), + modifier = modifier.fillMaxWidth(), + contentPadding = contentPadding, verticalArrangement = Arrangement.spacedBy( - space = VerticalSpacedBy, + space = horizontalSpace, ), ) { if (title != null) { @@ -679,8 +693,15 @@ public fun QuackLazyVerticalGridTag( } itemsIndexed( items = chunkedItems, - key = { _, item -> - item + key = { index: Int, items: List -> + key!!.invoke( + /* index = */ + index, + /* items = */ + items, + ) + }.takeIf { + key != null }, ) { rowIndex, rowItems -> Row( @@ -690,11 +711,11 @@ public fun QuackLazyVerticalGridTag( state = rememberScrollState(), ), horizontalArrangement = Arrangement.spacedBy( - space = HorizontalSpacedBy, + space = verticalSpace, ), ) { rowItems.fastForEachIndexed { index, item -> - val currentIndex = rowIndex * 2 + index + val currentIndex = rowIndex * itemChunkedSize + index val isSelected = itemSelections?.get( index = currentIndex, ) ?: false @@ -703,12 +724,18 @@ public fun QuackLazyVerticalGridTag( ) { when (this) { is QuackTagType.Grayscale -> QuackGrayscaleTagInternal( + modifier = Modifier.animateItemPlacement( + animationSpec = QuackAnimationSpec(), + ), text = item, trailingText = trailingText, actualIndex = currentIndex, onClickWithIndex = onClick, ) is QuackTagType.Circle -> QuackCircleTagInternal( + modifier = Modifier.animateItemPlacement( + animationSpec = QuackAnimationSpec(), + ), text = item, trailingIcon = trailingIcon, isSelected = isSelected, @@ -716,6 +743,9 @@ public fun QuackLazyVerticalGridTag( onClickWithIndex = onClick, ) QuackTagType.Round -> QuackRoundTagInternal( + modifier = Modifier.animateItemPlacement( + animationSpec = QuackAnimationSpec(), + ), text = item, isSelected = isSelected, actualIndex = currentIndex, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/toggle.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/toggle.kt index aa4e453ac..b8109e049 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/toggle.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/toggle.kt @@ -285,7 +285,7 @@ public fun QuackToggleButton( receiver = QuackToggleDefaults.ToggleButton, ) { Row( - modifier = modifier.wrapContentSize(), + modifier = modifier, horizontalArrangement = Arrangement.spacedBy( space = ItemSpacedBy, ), diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/typography.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/typography.kt index 0ddd409c3..27e9d351d 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/typography.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/component/typography.kt @@ -7,7 +7,8 @@ package team.duckie.quackquack.ui.component -import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -28,14 +29,63 @@ import team.duckie.quackquack.ui.component.internal.QuackText import team.duckie.quackquack.ui.modifier.quackClickable import team.duckie.quackquack.ui.textstyle.QuackTextStyle import team.duckie.quackquack.ui.util.Empty +import team.duckie.quackquack.ui.util.runIf // [Note] @NonRestartableComposable 안한 이유: 인자로 받은 Text 는 동적으로 바뀔 수 있음 +// TODO: 중복 코드 제거 + +/** + * [QuackText] 에 [QuackTextStyle.SplashSlogan] 스타일을 적용하여 + * 주어진 텍스트를 표시합니다. + * + * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. + * @param text 표시할 텍스트 + * @param color 텍스트의 색상 + * @param align 텍스트 정렬 + * @param rippleEnabled 텍스트 클릭시 ripple 발생 여부 + * @param singleLine 텍스트 표시에 한 줄만 사용할지 여부 + * @param overflow 텍스트를 한 줄에 표시할 수 없을 때 표시 방식 + * @param onClick 텍스트가 클릭됐을 때 실행할 람다식 + */ +@Composable +public fun QuackSplashSlogan( + modifier: Modifier = Modifier, + padding: PaddingValues? = null, + text: String, + color: QuackColor = QuackColor.Black, + align: TextAlign = TextAlign.Start, + rippleEnabled: Boolean = false, + singleLine: Boolean = false, + overflow: TextOverflow = TextOverflow.Ellipsis, + onClick: (() -> Unit)? = null, +): Unit = QuackText( + modifier = modifier + .quackClickable( + rippleEnabled = rippleEnabled, + onClick = onClick, + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, + text = text, + style = QuackTextStyle.SplashSlogan.change( + color = color, + textAlign = align, + ), + singleLine = singleLine, + overflow = overflow, +) /** * [QuackText] 에 [QuackTextStyle.HeadLine1] 스타일을 적용하여 * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -47,6 +97,7 @@ import team.duckie.quackquack.ui.util.Empty @Composable public fun QuackHeadLine1( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -56,11 +107,16 @@ public fun QuackHeadLine1( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.HeadLine1.change( color = color, @@ -75,6 +131,7 @@ public fun QuackHeadLine1( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -86,6 +143,7 @@ public fun QuackHeadLine1( @Composable public fun QuackHeadLine2( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -95,11 +153,16 @@ public fun QuackHeadLine2( overflow: TextOverflow = TextOverflow.Ellipsis, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.HeadLine2.change( color = color, @@ -114,6 +177,7 @@ public fun QuackHeadLine2( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -125,6 +189,7 @@ public fun QuackHeadLine2( @Composable public fun QuackTitle1( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -134,11 +199,16 @@ public fun QuackTitle1( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Title1.change( color = color, @@ -153,6 +223,7 @@ public fun QuackTitle1( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -164,6 +235,7 @@ public fun QuackTitle1( @Composable public fun QuackTitle2( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -173,11 +245,16 @@ public fun QuackTitle2( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Title2.change( color = color, @@ -192,6 +269,7 @@ public fun QuackTitle2( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -203,6 +281,7 @@ public fun QuackTitle2( @Composable public fun QuackSubtitle( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -212,11 +291,16 @@ public fun QuackSubtitle( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Subtitle.change( color = color, @@ -231,6 +315,7 @@ public fun QuackSubtitle( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -242,6 +327,7 @@ public fun QuackSubtitle( @Composable public fun QuackSubtitle2( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -251,11 +337,16 @@ public fun QuackSubtitle2( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Subtitle2.change( color = color, @@ -270,6 +361,7 @@ public fun QuackSubtitle2( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -281,6 +373,7 @@ public fun QuackSubtitle2( @Composable public fun QuackBody1( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -290,11 +383,16 @@ public fun QuackBody1( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Body1.change( color = color, @@ -309,6 +407,7 @@ public fun QuackBody1( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -320,6 +419,7 @@ public fun QuackBody1( @Composable public fun QuackBody2( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -329,11 +429,16 @@ public fun QuackBody2( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Body2.change( color = color, @@ -348,6 +453,7 @@ public fun QuackBody2( * 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param color 텍스트의 색상 * @param align 텍스트 정렬 @@ -359,6 +465,7 @@ public fun QuackBody2( @Composable public fun QuackBody3( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, @@ -368,11 +475,16 @@ public fun QuackBody3( onClick: (() -> Unit)? = null, ): Unit = QuackText( modifier = modifier - .wrapContentSize() .quackClickable( rippleEnabled = rippleEnabled, onClick = onClick, - ), + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, text = text, style = QuackTextStyle.Body3.change( color = color, @@ -386,6 +498,7 @@ public fun QuackBody3( * [QuackHeadLine2] 의 원하는 부분에 밑줄로 강조하여 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param underlineTexts 강조할 텍스트 리스트 * @param color 텍스트의 색상 @@ -398,6 +511,7 @@ public fun QuackBody3( @Composable public fun QuackUnderlineHeadLine2( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, underlineTexts: ImmutableList, color: QuackColor = QuackColor.Black, @@ -408,12 +522,16 @@ public fun QuackUnderlineHeadLine2( onClick: (() -> Unit)? = null, ) { QuackText( - modifier = modifier - .wrapContentSize() - .quackClickable( - rippleEnabled = rippleEnabled, - onClick = onClick, - ), + modifier = modifier.quackClickable( + rippleEnabled = rippleEnabled, + onClick = onClick, + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, annotatedText = rememberDecorationAnnotatedString( text = text, decorationTexts = underlineTexts, @@ -435,8 +553,10 @@ public fun QuackUnderlineHeadLine2( * [QuackUnderlineBody3] 의 원하는 부분에 밑줄로 강조하여 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param underlineTexts 강조할 텍스트 리스트들 + * @param underlineColor 강조할 색상 * @param color 텍스트의 색상 * @param align 텍스트 정렬 * @param rippleEnabled 텍스트 클릭시 ripple 발생 여부 @@ -447,8 +567,10 @@ public fun QuackUnderlineHeadLine2( @Composable public fun QuackUnderlineBody3( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, underlineTexts: ImmutableList, + underlineColor: QuackColor = QuackColor.DuckieOrange, color: QuackColor = QuackColor.Black, align: TextAlign = TextAlign.Start, rippleEnabled: Boolean = false, @@ -457,17 +579,21 @@ public fun QuackUnderlineBody3( onClick: (() -> Unit)? = null, ) { QuackText( - modifier = modifier - .wrapContentSize() - .quackClickable( - rippleEnabled = rippleEnabled, - onClick = onClick, - ), + modifier = modifier.quackClickable( + rippleEnabled = rippleEnabled, + onClick = onClick, + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, annotatedText = rememberDecorationAnnotatedString( text = text, decorationTexts = underlineTexts, decorationStyle = SpanStyle( - color = QuackColor.DuckieOrange.composeColor, + color = underlineColor.composeColor, textDecoration = TextDecoration.Underline, ), ), @@ -484,6 +610,7 @@ public fun QuackUnderlineBody3( * [QuackHighlightBody1] 의 원하는 부분에 SemiBold 로 강조하여 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param highlightTexts 강조할 텍스트 리스트들 * @param color 텍스트의 색상 @@ -496,6 +623,7 @@ public fun QuackUnderlineBody3( @Composable public fun QuackHighlightBody1( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, highlightTexts: ImmutableList, color: QuackColor = QuackColor.Black, @@ -506,12 +634,16 @@ public fun QuackHighlightBody1( onClick: (() -> Unit)? = null, ) { QuackText( - modifier = modifier - .wrapContentSize() - .quackClickable( - rippleEnabled = rippleEnabled, - onClick = onClick, - ), + modifier = modifier.quackClickable( + rippleEnabled = rippleEnabled, + onClick = onClick, + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, annotatedText = rememberDecorationAnnotatedString( text = text, decorationTexts = highlightTexts, @@ -574,6 +706,7 @@ private fun rememberDecorationAnnotatedString( * SemiBold + Underline 강조를 추가하여 주어진 텍스트를 표시합니다. * * @param modifier 컴포넌트에 적용할 [Modifier] + * @param padding 적용할 패딩. 클릭 영역을 늘리기 위해 사용될 수 있습니다. * @param text 표시할 텍스트 * @param highlightTextPairs 강조할 텍스트 및 그에 대응하는 클릭 이벤트를 나타내는 [Pair] 리스트 * @param color 텍스트의 색상 @@ -587,6 +720,7 @@ private fun rememberDecorationAnnotatedString( @Composable public fun QuackAnnotatedBody2( modifier: Modifier = Modifier, + padding: PaddingValues? = null, text: String, highlightTextPairs: ImmutableList Unit)?>>, color: QuackColor = QuackColor.Black, @@ -621,7 +755,7 @@ public fun QuackAnnotatedBody2( targetTextClickEvent?.let { // ClickEventTextInfo 데이터 추가 add( - element = HighlightTextInfo( + element = QuackHighlightTextInfo( text = targetText, startIndex = realStartIndex, endIndex = realEndIndex, @@ -637,12 +771,16 @@ public fun QuackAnnotatedBody2( } QuackClickableText( - modifier = modifier - .wrapContentSize() - .quackClickable( - rippleEnabled = rippleEnabled, - onClick = onClick, - ), + modifier = modifier.quackClickable( + rippleEnabled = rippleEnabled, + onClick = onClick, + ).runIf( + condition = padding != null, + ) { + padding( + paddingValues = padding!!, + ) + }, clickEventTextInfo = highlightTextInfo, text = buildAnnotatedString { append( @@ -695,7 +833,7 @@ public fun QuackAnnotatedBody2( * @param endIndex 전체 텍스트 내에서 onClick 이벤트가 실행되는 마지막 index * @param onClick 실행할 클릭 이벤트 (null 일 시 이벤트 없음) */ -internal data class HighlightTextInfo( +public data class QuackHighlightTextInfo( val text: String, val startIndex: Int, val endIndex: Int, diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/icon/QuackIcon.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/icon/QuackIcon.kt index fedab6162..226b4da7a 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/icon/QuackIcon.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/icon/QuackIcon.kt @@ -30,6 +30,10 @@ public value class QuackIcon private constructor( drawableId = R.drawable.quack_duckie_text_logo, ) + public val Check: QuackIcon = QuackIcon( + drawableId = R.drawable.quack_ic_check_24, + ) + public val Share: QuackIcon = QuackIcon( drawableId = R.drawable.quack_ic_share_24, ) diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/modifier/clickable.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/modifier/clickable.kt index 6e0cc27fc..575f41423 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/modifier/clickable.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/modifier/clickable.kt @@ -5,9 +5,12 @@ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE */ +@file:OptIn(ExperimentalFoundationApi::class) + package team.duckie.quackquack.ui.modifier -import androidx.compose.foundation.clickable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.getValue @@ -45,11 +48,13 @@ public var QuackAlwaysShowRipple: Boolean by mutableStateOf( * 이 [Modifier] 는 꽥꽥 컴포넌트의 중간 단계에서도 사용될 수 있으므로 * 기본값이 정의됬습니다. * - * @param onClick 컴포넌트가 클릭됐을 때 실행할 람다식. - * null 이 들어오면 clickable 을 설정하지 않습니다. * @param rippleEnabled 리플을 설정할지 여부 * @param rippleColor 표시될 리플의 색상. * null 이 들어오면 [Color.Unspecified] 를 사용합니다. + * @param onClick 컴포넌트가 클릭됐을 때 실행할 람다식. + * null 이 들어오면 clickable 을 설정하지 않습니다. + * @param onLongClick 컴포넌트가 길게 클릭됐을 떄 실행할 람다식. + * null 이 들어오면 long-clickable 을 설정하지 않습니다. * * @return clickable 속성이 적용된 [Modifier] */ @@ -57,13 +62,15 @@ public var QuackAlwaysShowRipple: Boolean by mutableStateOf( public fun Modifier.quackClickable( rippleEnabled: Boolean = true, rippleColor: QuackColor? = null, + onLongClick: (() -> Unit)? = null, onClick: (() -> Unit)?, ): Modifier = runIf( condition = onClick != null, ) { composed { - clickable( + combinedClickable( onClick = onClick!!, + onLongClick = onLongClick, indication = rememberRipple( color = rippleColor?.composeColor ?: Color.Unspecified, ).takeIf { diff --git a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/textstyle/QuackTextStyle.kt b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/textstyle/QuackTextStyle.kt index f2c424c25..00febffa7 100644 --- a/ui-components/src/main/kotlin/team/duckie/quackquack/ui/textstyle/QuackTextStyle.kt +++ b/ui-components/src/main/kotlin/team/duckie/quackquack/ui/textstyle/QuackTextStyle.kt @@ -47,7 +47,7 @@ import team.duckie.quackquack.ui.util.AllowMagicNumber * @param lineHeight 텍스트 줄 크기 * @param textAlign 텍스트 align. 기본값은 Center 입니다. */ -// animateQuackTextStyleAsState() 있어서 internal constructor +// animatedQuackTextStyleAsState 있어서 internal constructor public class QuackTextStyle internal constructor( public val color: QuackColor = QuackColor.Black, public val size: TextUnit, @@ -112,6 +112,17 @@ public class QuackTextStyle internal constructor( } public companion object { + /** + * 덕키 스플래시 화면의 슬로건에 쓰이는 특이 케이스의 TextStyle + */ + public val SplashSlogan: QuackTextStyle = QuackTextStyle( + size = 24.sp, + weight = FontWeight.Bold, + letterSpacing = 0.sp, + lineHeight = 32.sp, + textAlign = TextAlign.Left, + ) + public val HeadLine1: QuackTextStyle = QuackTextStyle( size = 20.sp, weight = FontWeight.Bold, diff --git a/ui-components/src/main/res/drawable/quack_ic_check_24.xml b/ui-components/src/main/res/drawable/quack_ic_check_24.xml new file mode 100644 index 000000000..0ae6723df --- /dev/null +++ b/ui-components/src/main/res/drawable/quack_ic_check_24.xml @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/versions/ui-components.txt b/versions/ui-components.txt index 675e8a6d2..c4cb3b0b3 100644 --- a/versions/ui-components.txt +++ b/versions/ui-components.txt @@ -1,3 +1,3 @@ major=1 -minor=2 -patch=5 +minor=3 +patch=1