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

Floating Action Button #85

Merged
merged 40 commits into from
Sep 11, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
bc6cb5c
test: 스냅샷 테스트
EvergreenTree97 Sep 3, 2022
ba17c33
feat: click 이벤트 없는 icon
EvergreenTree97 Sep 3, 2022
b1d02b7
feat; fab버튼 dialog 수정 필요
EvergreenTree97 Sep 3, 2022
5c9e5c7
Merge branch 'develop' of https://github.com/sungbinland/duckie-quack…
EvergreenTree97 Sep 4, 2022
d9f0696
feat: fab 버튼 구현
EvergreenTree97 Sep 4, 2022
6219c47
feat: icon 추가
EvergreenTree97 Sep 4, 2022
a599467
feat: icon 추가
EvergreenTree97 Sep 4, 2022
b3c7089
test: 스냅샷 테스트
EvergreenTree97 Sep 4, 2022
54927c1
remove: nonClickableImage 삭제
EvergreenTree97 Sep 4, 2022
ee10633
test: 스냅샷 테스트 네이밍 변경
EvergreenTree97 Sep 4, 2022
98e2d57
refactor: quackClickable 사용 및 NonclickImageIcon 삭제
EvergreenTree97 Sep 4, 2022
c885f3b
refactor : 변수와 함수 사이 empty line 추가
EvergreenTree97 Sep 6, 2022
8b4b24b
refactor: kdoc 수정 및 불필요한 람다 생성 제거
EvergreenTree97 Sep 6, 2022
1e5fad9
feat: Playground 추가
EvergreenTree97 Sep 6, 2022
3c28e78
refactor : 불필요한 속성 명시 제거
EvergreenTree97 Sep 6, 2022
1caa739
style: resource 코드 정렬
EvergreenTree97 Sep 6, 2022
bb084f4
refactor: offset 함수 변경
EvergreenTree97 Sep 6, 2022
1854bbb
refactor: 불필요한 null 제거
EvergreenTree97 Sep 6, 2022
04b81ca
refactor: QuackText에 style 주는 대신 대신 QuackSubTitle 사용
EvergreenTree97 Sep 6, 2022
f82179e
refactor: Playground argument 변경
EvergreenTree97 Sep 6, 2022
d7f2a63
refactor: expanded 제어권 제공
EvergreenTree97 Sep 6, 2022
17c816e
test: 스냅샷 테스트 expanded parameter로 제공
EvergreenTree97 Sep 6, 2022
7d751b7
refactor: Spacer 제거 후 padding으로 대체
EvergreenTree97 Sep 6, 2022
0945183
refactor: QuackPopupItem -> QuackDialogItem 네이밍 변경 및 DialogMenu Conte…
EvergreenTree97 Sep 6, 2022
c6b45cf
feat: Item과 Text 사이 간격 추가
EvergreenTree97 Sep 6, 2022
7a7269c
refactor: QuackPopupItem -> QuackDialogItem으로 변경
EvergreenTree97 Sep 6, 2022
f5b85bd
refactor: Column 에서 LazyColumn 으로 변경
EvergreenTree97 Sep 6, 2022
9e72012
refactor: @NonRestartableComposable 불필요한 것들 제거하고 필요한 것 추가
EvergreenTree97 Sep 6, 2022
146fe05
feat: class에 hashcode와 equals 추가
EvergreenTree97 Sep 6, 2022
38099fc
feat: AnimatedVisibility 적용
EvergreenTree97 Sep 6, 2022
ca63030
refactor: Item의 중복 클릭 이벤트 없애고, (QuackDialogMenuItem) -> Unit 람다 추가
EvergreenTree97 Sep 6, 2022
a301b21
test: 파라미터 변경에 따라 람다 추가
EvergreenTree97 Sep 6, 2022
1a6a8ff
refactor: named arguments 추가
EvergreenTree97 Sep 6, 2022
83729e3
docs: 파라미터 변경에 따른 Kdoc 변경
EvergreenTree97 Sep 6, 2022
2cc59d4
Merge branch 'develop' of https://github.com/sungbinland/duckie-quack…
EvergreenTree97 Sep 8, 2022
f8ed180
refactor: fab offset,size 람다로 구현
EvergreenTree97 Sep 8, 2022
3ac360b
refactor: Playground Box로 변경
EvergreenTree97 Sep 8, 2022
fe588ee
fix: NewLine 추가
EvergreenTree97 Sep 8, 2022
27071e0
refactor: lint module 린트 에러
EvergreenTree97 Sep 8, 2022
1833c3b
refactor: offset 구하는거 kdoc에 자세히 명시 및 린트 에러 수정
EvergreenTree97 Sep 8, 2022
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,356 @@
/*
* Designed and developed by 2022 SungbinLand, Team Duckie
*
* [fab.kt] created by Ji Sungbin on 22. 9. 3. 오후 1:15
*
* Licensed under the MIT.
* Please see full license: https://github.com/sungbinland/quack-quack/blob/main/LICENSE
*/

package team.duckie.quackquack.ui.component

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.collections.immutable.PersistentList
import team.duckie.quackquack.ui.color.QuackColor
import team.duckie.quackquack.ui.constant.QuackHeight
import team.duckie.quackquack.ui.constant.QuackWidth
import team.duckie.quackquack.ui.icon.QuackIcon
import team.duckie.quackquack.ui.modifier.applyQuackSize
import team.duckie.quackquack.ui.modifier.quackClickable
import team.duckie.quackquack.ui.textstyle.QuackTextStyle

private val QuackFabSize = 48.dp

private val QuackFabShape = CircleShape
private val QuackPopUpMenuShape = RoundedCornerShape(12.dp)

private val QuackMenuTopPadding = 20.dp
private val QuackMenuHorizontalPadding = 16.dp
private val QuackFabItemPadding = 16.dp

private val QuackFabItemSpacing = 8.dp

@Immutable
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
class QuackPopUpMenuItem(
val quackIcon: QuackIcon,
val text: String,
val onClick: () -> Unit,
)

EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
/**
* QuackMenuFloatingActionButton를 구현하였습니다.
*
* [QuackFloatingActionButton] 과는 다르게, 버튼을 클릭하였을 때
* DialogMenu가 출력됩니다.
* [QuackPopUpMenuItem]를 통해 메뉴 아이템에 들어갈 icon, text, onClick을 설정할 수 있습니다.
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param items 버튼을 클릭했을 때 나오는 메뉴의 아이템 리스트[QuackPopUpMenuItem]
* @see QuackPopUpMenuItem
*/
@Composable
fun QuackMenuFloatingActionButton(
items: PersistentList<QuackPopUpMenuItem>,
) {
var expanded by remember { mutableStateOf(false) }
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
var positionInRoot by remember { mutableStateOf(Offset.Zero) }
var menuSize by remember { mutableStateOf(IntSize.Zero) }
var buttonSize by remember { mutableStateOf(IntSize.Zero) }
QuackBasicFloatingActionButton(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
modifier = Modifier
.onGloballyPositioned { layoutCoordinates ->
positionInRoot = layoutCoordinates.positionInWindow()
buttonSize = layoutCoordinates.size
},
icon = QuackIcon.Plus,
onClick = {
expanded = true
},
)
if (expanded) {
QuackDialog(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
buttonOffset = positionInRoot,
menuSize = menuSize,
buttonSize = buttonSize,
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
onDismissRequest = {
expanded = false
},
) {
Column(
modifier = Modifier.onGloballyPositioned { layoutCoordinates ->
menuSize = layoutCoordinates.size
},
horizontalAlignment = Alignment.End,
) {
QuackDialogMenu(
items = items,
)
Spacer(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
modifier = Modifier.height(
height = QuackFabItemSpacing,
),
)
QuackBasicFloatingActionButton(
icon = QuackIcon.Close,
onClick = {
expanded = false
},
)
}
}
}
}

/**
* QuackMenuFloatingActionButton를 클릭했을 때 나오는 다이얼로그 입니다.
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
*
* Dialog는 내부적으로 Android Dialog로 구현되어있고, Compose View로 래핑되어 있습니다.
* 따라서 위치를 수동으로 조절할 수는 없고, Full Size Box에서 offset으로 조정되어야 합니다.
*
* 따라서 FloatingActionButton의 Offset을 구한 다음,
* Menu 크기만큼 더하고 Button 크기만큼 빼면 위치를 조정시킬 수 있습니다.
*
* dpOffsetX - menuWidth + buttonWidth
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
*
* @param buttonOffset FloatingActionButton의 offset
* @param menuSize menu container 크기
* @param buttonSize button container 크기
* @param onDismissRequest Menu를 닫으라는 명령이 떨어졌을 때의 동작
* @param content Dialog 내부에 들어갈 Composable
*/
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun QuackDialog(
buttonOffset: Offset,
menuSize: IntSize,
buttonSize: IntSize,
onDismissRequest: () -> Unit,
content: @Composable () -> Unit,
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = DialogProperties(
dismissOnBackPress = true,
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
usePlatformDefaultWidth = false,
),
) {
val dpOffsetX = pixelToDp(
pixel = buttonOffset.x
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
)
val dpOffsetY = pixelToDp(
pixel = buttonOffset.y
)
val menuWidth = pixelToDp(
pixel = menuSize.width.toFloat()
)
val menuHeight = pixelToDp(
pixel = menuSize.height.toFloat()
)
val buttonWidth = pixelToDp(
pixel = buttonSize.width.toFloat()
)
val buttonHeight = pixelToDp(
pixel = buttonSize.height.toFloat()
)
Box(
modifier = Modifier
.fillMaxSize()
.clickable {
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
onDismissRequest()
}
.offset(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
x = dpOffsetX - menuWidth + buttonWidth,
y = dpOffsetY - menuHeight + buttonHeight / 2,
),
contentAlignment = Alignment.TopStart,
) {
Box {
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
content()
}
}
}
}

/**
* QuackDialogMenu를 구현하였습니다.
*
* 메뉴의 아이템 리스트를 보여주는 Composable 입니다.
*
* @param items 버튼을 클릭했을 때 나오는 메뉴의 아이템 리스트[QuackPopUpMenuItem]
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
* @see QuackPopUpMenuItem
*/
@Composable
@NonRestartableComposable
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
private fun QuackDialogMenu(
items: PersistentList<QuackPopUpMenuItem>,
) {
QuackSurface(
modifier = Modifier.applyQuackSize(
width = QuackWidth.Wrap,
height = QuackHeight.Wrap,
),
shape = QuackPopUpMenuShape,
backgroundColor = QuackColor.White,
) {
Column(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
modifier = Modifier
.padding(
horizontal = QuackMenuHorizontalPadding,
)
.padding(
top = QuackMenuTopPadding,
),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
items.forEach { item: QuackPopUpMenuItem ->
QuackDialogMenuContent(
itemIcon = item.quackIcon,
itemText = item.text,
onClickItem = item.onClick,
)
Spacer(
modifier = Modifier.height(
height = QuackFabItemPadding,
),
)
}
}
}
}

/**
* QuackDialogMenuContent를 구현하였습니다.
*
* 메뉴의 Item List Content 입니다.
*
* @param itemIcon item의 icon
* @param itemText item의 text
* @param onClickItem item의 클릭 이벤트
*/
@Composable
@NonRestartableComposable
internal fun QuackDialogMenuContent(
itemIcon: QuackIcon,
itemText: String,
onClickItem: () -> Unit,
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
) {
Row(
modifier = Modifier.quackClickable {
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
onClickItem()
},
verticalAlignment = Alignment.CenterVertically,
) {
QuackImage(
icon = itemIcon,
onClick = null,
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
)
QuackText(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
text = itemText,
style = QuackTextStyle.Subtitle,
)
}
}

/**
* QuackFloatingActionButton을 구현하였습니다.
*
* Box로 구현되어 있기 때문에, 다른 Composable 위에 배치될 수 있습니다.
*
* @param icon FloatingActionButton에 들어갈 icon
* @param onClick 버튼 클릭 이벤트
*/
@Composable
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
fun QuackFloatingActionButton(
icon: QuackIcon,
onClick: () -> Unit,
) {
QuackBasicFloatingActionButton(
icon = icon,
onClick = onClick,
)
}

/**
* QuackFloatingActionButton의 기초가 되는 Composable
*
* Content의 사이즈를 알아야 하는 경우로 인해 Modifier를 가집니다.
*
* @param modifier [Modifier]
* @param icon FloatingActionButton에 들어갈 icon
* @param onClick 버튼 클릭 이벤트
*/
@Composable
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
private fun QuackBasicFloatingActionButton(
modifier: Modifier = Modifier,
icon: QuackIcon,
onClick: () -> Unit,
) {
Box(
modifier = modifier
.size(
QuackFabSize,
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
)
.clip(
shape = QuackFabShape,
)
.background(
color = QuackColor.DuckieOrange.value,
)
.quackClickable(
onClick = onClick,
),
contentAlignment = Alignment.Center,
) {
QuackImage(
tint = QuackColor.White,
icon = icon,
onClick = null,
)
}
}

/**
* pixel로 주어지는 offset을 dp로 변경하기 위한 function
*
* @param pixel dp로 변경할 pixel
*/
@Composable
@Stable
internal fun pixelToDp(pixel: Float) = with(
EvergreenTree97 marked this conversation as resolved.
Show resolved Hide resolved
receiver = LocalDensity.current,
) {
pixel.toDp()
}
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,20 @@ value class QuackIcon private constructor(
val Heart = QuackIcon(
drawableId = R.drawable.ic_heart_24,
)

@Stable
val WriteFeed = QuackIcon(
drawableId = R.drawable.ic_write_feed_24,
)

@Stable
val DrawerBuy = QuackIcon(
drawableId = R.drawable.ic_drawer_buy_24,
)

@Stable
val DMNew = QuackIcon(
drawableId = R.drawable.ic_dm_new_20_white,
)
}
}
Loading