Android Compose Animation

나타남 / 사라짐 애니메이션

AnimatedVisibility의 enter 및 exit 매개변수를 사용하면 컴포저블이 표시되고 사라질 때의 동작을 구성할 수 있습니다. 자세한 내용은 전체 문서를 참고하세요.

컴포저블의 가시성을 애니메이션화하는 또 다른 방법은 animateFloatAsState를 사용하여 시간에 따라 알파를 애니메이션화하는 것입니다

var visible by remember {
    mutableStateOf(true)
}
val animatedAlpha by animateFloatAsState(
    targetValue = if (visible) 1.0f else 0f,
    label = "alpha"
)
Box(
    modifier = Modifier
        .size(200.dp)
        .graphicsLayer {
            alpha = animatedAlpha
        }
        .clip(RoundedCornerShape(8.dp))
        .background(colorGreen)
        .align(Alignment.TopCenter)
) {
}


하지만 알파를 변경하면 컴포저블이 컴포지션에 남아 레이아웃된 공간을 계속 차지한다는 주의사항이 있습니다. 

이로 인해 스크린 리더와 기타 접근성 메커니즘에서 화면의 항목을 계속 고려할 수 있습니다. 

반면 AnimatedVisibility는 결국 컴포지션에서 항목을 삭제합니다.



AnimatedVisibility의 enter 및 exit 매개변수를 사용하면 컴포저블이 표시되고 사라질 때의 동작을 구성할 수 있습니다. 자세한 내용은 전체 문서를 참고하세요.

컴포저블의 가시성을 애니메이션화하는 또 다른 방법은 animateFloatAsState를 사용하여 시간에 따라 알파를 애니메이션화하는 것입니다


if (animateBackgroundColor) colorGreen else colorBlue,
    label = "color"
)
Column(
    modifier = Modifier.drawBehind {
        drawRect(animatedColor)
    }
) {
    // your composable here
}


var expanded by remember { mutableStateOf(false) }
Box(
    modifier = Modifier
        .background(colorBlue)
        .animateContentSize()
        .height(if (expanded) 400.dp else 200.dp)
        .fillMaxWidth()
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            expanded = !expanded
        }

) {
}



컴포저블의 위치를 애니메이션으로 표시하려면 animateIntOffsetAsState()과 함께 Modifier.offset{ }를 사용합니다.

var moved by remember { mutableStateOf(false) }
val pxToMove = with(LocalDensity.current) {
    100.dp.toPx().roundToInt()
}
val offset by animateIntOffsetAsState(
    targetValue = if (moved) {
        IntOffset(pxToMove, pxToMove)
    } else {
        IntOffset.Zero
    },
    label = "offset"
)

Box(
    modifier = Modifier
        .offset {
            offset
        }
        .background(colorBlue)
        .size(100.dp)
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            moved = !moved
        }
)




컴포저블의 패딩을 애니메이션으로 처리하려면 animateDpAsState를 Modifier.padding()와 함께 사용합니다.

var toggled by remember {
    mutableStateOf(false)
}
val animatedPadding by animateDpAsState(
    if (toggled) {
        0.dp
    } else {
        20.dp
    },
    label = "padding"
)
Box(
    modifier = Modifier
        .aspectRatio(1f)
        .fillMaxSize()
        .padding(animatedPadding)
        .background(Color(0xff53D9A1))
        .clickable(
            interactionSource = remember { MutableInteractionSource() },
            indication = null
        ) {
            toggled = !toggled
        }
)




val infiniteTransition = rememberInfiniteTransition(label = "infinite transition")
val scale by infiniteTransition.animateFloat(
    initialValue = 1f,
    targetValue = 8f,
    animationSpec = infiniteRepeatable(tween(1000), RepeatMode.Reverse),
    label = "scale"
)
Box(modifier = Modifier.fillMaxSize()) {
    Text(
        text = "Hello",
        modifier = Modifier
            .graphicsLayer {
                scaleX = scale
                scaleY = scale
                transformOrigin = TransformOrigin.Center
            }
            .align(Alignment.Center),
        // Text composable does not take TextMotion as a parameter.
        // Provide it via style argument but make sure that we are copying from current theme
        style = LocalTextStyle.current.copy(textMotion = TextMotion.Animated)
    )
}



구글문서참고

 https://developer.android.com/develop/ui/compose/animation/quick-guide?_gl=1*bqkue9*_up*MQ..*_ga*MTQ2NDcwNjM0OS4xNzMxMTk1NDY2*_ga_6HH9YJMN9M*MTczMTE5NTQ2NS4xLjAuMTczMTE5NTQ2NS4wLjAuMTU2MDEwNTc2Mw..&hl=ko

Jetpack Compose Custom CameraView 촬영-프리뷰 구현하기

현재 서비스중인 앱의 카메라 기능은 전부 XML 기반의 커스텀뷰를 만들어서 사용중이다.

Compose를 공부하는 기회에 해당 부분을 Compose로 구현해 봤다.


정말 간단한 기능의 카메라 촬영, 프리뷰 기능만을 구현한 코드를 기록해 두려한다.

코드의 가독성이나 이슈가될 만한 부분들은 상황에 맞게 조절하여 사용하시면 됩니다.


@Composable
fun CameraComponent(
    modifier: Modifier = Modifier,
    selfMode: Boolean,
    takeAction: Boolean,
    receiveImageUrl: (Bitmap?) -> Unit
) {
    val cameraSelector = if (selfMode) {
        CameraSelector.DEFAULT_FRONT_CAMERA
    } else {
        CameraSelector.DEFAULT_BACK_CAMERA
    }
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val imageCapture = remember { ImageCapture.Builder().build() }
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }
    val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
    val preview: androidx.camera.core.Preview = androidx.camera.core.Preview.Builder().build()

    val cameraViewModel: CameraComponentModel = hiltViewModel()
    val imageBitmap by cameraViewModel.imageBitmap.collectAsState()
    var processing: Boolean by remember { mutableStateOf(false) }

    // 카메라 바인딩
    // cameraProvider를 통해 ProcessCameraProvider를 비동기적으로 가져와 카메라의 생명주기를 현재 카메라 컴포넌트 생명주기와 바인딩
    // preview, imageCapture를 lifecycleOwner에 바인딩하여 카메라 미리보기와 이미지 캡쳐를 구현
    // 카메라 세션 준비가 완료되면 프리뷰를 화면에 표시
    LaunchedEffect(cameraProviderFuture, cameraSelector) {
        cameraProviderFuture.addListener({
            try {
                cameraProvider.unbindAll()

                cameraProvider.bindToLifecycle(
                    lifecycleOwner,
                    cameraSelector,
                    preview,
                    imageCapture
                )
            } catch (exc: Exception) {
                Timber.e("Camera binding failed")
            }
        }, ContextCompat.getMainExecutor(context))
    }

    // takeAction 상태가 true로 변경될 때, imageCapture의 takePicture 메서드를 호출하여 이미지 캡쳐
    // 촬영 성공 시, onCaptureSuccess 콜백에서 이미지를 비트맵으로 변환하고, 사용자 지정 작업 수행
    // 실패 시, onError 콜백에서 실패 이유를 로그로 출력하고, 사용자 지정 작업 수행
    LaunchedEffect(takeAction, imageBitmap) {
        if (takeAction && !processing) {
            imageCapture.takePicture(
                ContextCompat.getMainExecutor(context),
                object : ImageCapture.OnImageCapturedCallback() {
                    override fun onCaptureSuccess(image: ImageProxy) {
                        processing = true
                        cameraViewModel.processImageCapture(image, selfMode)
                    }

                    override fun onError(exception: ImageCaptureException) {
                        Timber.e("Image capture failed ${exception.message}")
                        receiveImageUrl(null)
                    }
                }
            )
        }
        if (imageBitmap != null) {
            receiveImageUrl(imageBitmap)
            cameraViewModel.clearImageBitmap()
            processing = false
        }
    }

    AndroidView(
        modifier = modifier.clipToBounds(),
        factory = { ctx ->
            PreviewView(ctx).apply {
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
            }
        },
        update = { previewView ->
            preview.setSurfaceProvider(previewView.surfaceProvider)
        }
    )
}


카메라 비율을 일정하게 고정시키고 싶으면 clipToBounds() 부분을 지우면 된다.

Image를 프레임에 맞춰 자르기 위해 modifier.clipToBounds() 를 통해 조절해 봤다.

Jetpack Compose Text 2편

저번 Text 설명 1편에 이어서 2편을 작성해보도록 하겠습니다.

Text는 크게 색다로운 내용이 있다긴 보다 되짚어보는 정리정도 되는거 같습니다. 

2편 같은 경우 사용자 상호작용 개념이 있어서 1편보다는 좀더 좋은 기능을 설명하고 있습니다.


1. 사용자 상호작용

 사용자 상호작용을 사용 설정할 수 있는 다양한 API를 설명합니다.

 - 여러 Composable 레이아웃에서 더 유연하게 텍스트를 선택할 수 있음

 - Text Composable 의 부분에 제어자를 추가할 수 없어서 Text의 사용자 상호작용은 다른 Composable 레이아웃과 다름


1) 텍스트 선택하기

기본적으로 Composable은 선택할 수 없습니다.


텍스트 선택 기능을 사용 설정하려면 Text를 SelectionContainer Composable로 래핑해야 합니다.

@Composable
fun SelectableText() {
    SelectionContainer {
        Text("This text is selectable")
    }
}
특정 부분만 선택 표시를 중지 할 수도 있습니다. 

선택 불가능한 부분을 DisableSelection Composable로 래핑해서 사용할 수 있습니다.

@Composable
fun PartiallySelectableText() {
    SelectionContainer {
        Column {
            Text("This text is selectable")
            Text("This one too")
            Text("This one as well")
            
            DisableSelection {
                Text("But not this one")
                Text("Neither this one")
            }
            
            Text("But again, you can select this one")
            Text("And this one too")
        }
    }
}


2) 텍스트에서 클릭 위치 가져오기

Text의 클릭을 수신 대기하려면 clickable  추가하면 됩니다.

Text Composable 내에서 클릭 위치를 가져오려면  ClickableText를 Text Compsable 대신 사용합니다.

@Composable
fun SimpleClickableText() {
    ClickableText(
        text = AnnotatedString("Click Me"),
        onClick = { offset ->
            Log.d("ClickableText", "$offset -th character is clicked.")
        }
    )
}


3) 주석(tag)이 추가된 클릭

사용자가 Text Composable을 클릭할 때 특정 단어에 연결된 하이퍼링크 같이 Text 값에 정보를 추가할 수 있습니다.

- tag(String), annotation(String), 텍스트 범위를 매개변수로 사용하는 주석을 추가해야 함

- AnnotatedString에서 이러한 주석을 태그 또는 텍스트 범위로 필터링

- buildAnnotatedString은 AnnotatedString 객체를 만드는 inline function이다.

 buildAnnotatedString을 이용해 만들어진 AnnotatedString은 하나의 Text 안에서 여러 스타일을 적용할 수 있도록 하는 정보를 담은 객체로 AnnotatedString 속에는 텍스트의 스타일에 대한 정보를 담은 text, spanStyles, paragraphStyles, annotations 객체들이 저장되어 있다. 

@Composable
fun AnnotatedClickableText() {
    val uriHandler = LocalUriHandler.current

    val annotatedText = buildAnnotatedString {
        append("Click ")

        // We attach this *URL* annotation to the following content
        // until `pop()` is called
        pushStringAnnotation(tag = "URL", annotation = "https://developer.android.com")
        withStyle(style = SpanStyle(color = Color.Blue, fontWeight = FontWeight.Bold)) {
            append("here")
        }

        pop()
    }

    ClickableText(
        text = annotatedText,
        onClick = { offset ->
            // We check if there is an *URL* annotation attached to the text
            // at the clicked position
            annotatedText.getStringAnnotations(tag = "URL", start = offset, end = offset)
                .firstOrNull()?.let { annotation ->
                    Log.d("Clicked URL", annotation.item)
                    uriHandler.openUri(annotation.item)
                }
        }
    )
}



2. 텍스트 입력 및 수정

- TextField를 사용하면 유저가 텍스트를 입력하거나 수정할 수 있습니다.

- 디자인이 Material TextField 또는 OutlineTextField를 호출하는 경우 TextField를 사용하는 할것을 권장

- 머티리얼 사양의 장식이 필요하지 않은 디자인을 빌드하는 경우 BasicTextField를 사용해야함


(1) TextField는 머티리얼 디자인 가이드라인을 따라 구현됨

   - 기본 스타일이 채워짐

   - OutlinedTextField는 위 스타일 버전입니다.


(2) BasicTextField는 유저가 키보드를 통해 텍스트를 수정할 수 있지만 힌트나 자리표시자 등등은 제공하지 않음


예제1. TextField

@Composable
fun SimpleFilledTextFieldSample() {
    var text by remember { mutableStateOf("Hello") }

    TextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}


예제2. outlinedTextField

@Composable
fun SimpleOutlinedTextFieldSample() {
    var text by remember { mutableStateOf("") }

    OutlinedTextField(
        value = text,
        onValueChange = { text = it },
        label = { Text("Label") }
    )
}

1) TextField 스타일 지정

TextField 및 BasicTextField는 커스텀 할 수 있는 많은 공통 매개변수를 공유합니다. 


TextField의 전체 목록은 아래링크에서 확인할 수 있습니다.

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/material/material/src/commonMain/kotlin/androidx/compose/material/TextField.kt;l=149?hl=ko



다음은 자주 사용하는 매개변수 목록입니다.

- singleLine

- maxLines

- textStyle

@Composable
fun StyledTextField() {
    var value by remember { mutableStateOf("Hello\nWorld\nInvisible") }

    TextField(
        value = value,
        onValueChange = { value = it },
        label = { Text("Enter text") },
        maxLines = 2,
        textStyle = TextStyle(color = Color.Blue, fontWeight = FontWeight.Bold),
        modifier = Modifier.padding(20.dp)
    )
}


2) 키보드 옵션

TextField를 사용하면 키보드 레이아웃과 같은 키보드 구성 옵션을 설정하거나 키보드에서 지원하는 경우


이래 지원되는 키보드 옵션들 입니다.

capitalization

autoCorrect

keyboardType

imeAction


위 자세한 설명은 이 링크를 참조 

https://developer.android.com/reference/kotlin/androidx/compose/foundation/text/KeyboardOptions?hl=ko#KeyboardOptions(androidx.compose.ui.text.input.KeyboardCapitalization,kotlin.Boolean,androidx.compose.ui.text.input.KeyboardType,androidx.compose.ui.text.input.ImeAction) 



3) 형식 지정

- TextField를 사용하면 입력 값에 형식을 설정할 수 있습니다. 

- 비밀번호를 나타내는 문자를 *로 바꾸거나 신용카드 번호의 4자리마다 하이픈을 삽입할 수 있습니다.

@Composable
fun PasswordTextField() {
    var password by rememberSaveable { mutableStateOf("") }

    TextField(
        value = password,
        onValueChange = { password = it },
        label = { Text("Enter password") },
        visualTransformation = PasswordVisualTransformation(),
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password)
    )
}


4) Focus 처리

많은 사용자들이 앱에서 텍스트를 타이핑을 한 후 키보드가 아닌 다른 곳을 터치하여 키보드를 닫는 UX에 익숙할 것입니다. 

이전까지는 이 처리를 위해 Activity에서 currentFocus를 이용해 Focus를 해제하는 방식을 사용했습니다.

//Activity에서 currentFocus를 이용해 Focus를 해제하는 방식
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    if (currentFocus is EditText) {
        SoftInputUtils.hideSoftInput(currentFocus)
        currentFocus!!.clearFocus()
    }
    return takeDispatchTouchEvent(ev)
}


그렇지만 TextField는 Composable 함수이기 때문에 위와 같이 특정 객체인지 판단할 수 없었습니다. 

그래서 TextField의 포커싱 여부를 판단하는 변수를 하나 두어 처리해야 했습니다.

어렵지 않은 작업이었지만 이 처리를 하면서 명령형 UI와 선언형 UI의 차이가 조금씩 체감이 되었습니다.

var isTextFieldFocused = false

@Composable
fun MainContent(vm: MainViewModel) {
    val focusRequester by remember { mutableStateOf(FocusRequester()) }

    BasicTextField(
        value = ...,
        modifier = Modifier
            .focusRequester(focusRequester = focusRequester)
            .onFocusChanged {
                isTextFieldFocused = it.isFocused
            }
    )
}

override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
    if (isTextFieldFocused) {
        SoftInputUtils.hideSoftInput(currentFocus)
        currentFocus!!.clearFocus()
    }
    return takeDispatchTouchEvent(ev)
}


위의 방식은 단점이 있다. TextField 가 늘어날 때마다 변수를 새로 생성해 주어야 한다.

다음의 방식으로 해결이 가능하다.

TextField가 아닌, 그것을 감싸고 있는 전체 화면의 터치 이벤트를 캐치해서 포커스를 해제하는 방법이 있다.

detectTapGestures로 태핑을 감지하여 포커스를 해제하면 된다.

Composable을 깔끔하게 쓰기 위해 Modifier의 Extension에 함수 추가

// 터치 이벤트 감지하여 포커스 해제
fun Modifier.addFocusCleaner(focusManager: FocusManager, doOnClear: () -> Unit = {}): Modifier {
    return this.pointerInput(Unit) {
        detectTapGestures(onTap = {
            doOnClear()
            focusManager.clearFocus()
        })
    }
}


그리고 TextField를 Column으로 감싸주고, Column을 터치할 때 위에 만들어준 함수로 포커스를 해제하도록 만들었다.

@Composable
fun MainContent() {
    val focusRequester by remember { mutableStateOf(FocusRequester()) }
    val focusManager = LocalFocusManager.current
    var text by remember { mutableStateOf("안뇽") }

    Column(modifier = Modifier
        .fillMaxSize()
        .addFocusCleaner(focusManager) //최상단 전체화면의 터치이벤트 감지하여 해제
    ) {
        TextField(
            value = text,
            onValueChange = {
                text = it
            },
            modifier = Modifier
                .focusRequester(focusRequester = focusRequester)
        )
    }
}




아래 링크에서 더 다양한 예제들을 볼 수 있습니다. 

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/ui/ui-text/samples/src/main/java/androidx/compose/ui/text/samples/VisualTransformationSamples.kt?hl=ko



**정리

- Compose 의 클릭영역은 SelectionContainer Composable로 TextComposable를 감싸서 만들면 사용할 수 있게 됨 

- SelectionContainer 안에서 클릭영역 허용하지 않을 공간을 DisableSelection Composable로 설정할 수 있음

- Text대신 CilckableText Composable 를 사용하면 유저가 클릭한 좌표를 가져올 수 있음 

- 추가된 클릭 표시를 하려면 AnnotatedString를 사용해서 tag , anotation 매개변수를 설정하면 됨 

- TextField 사용하면 유저가 글을 수정할 수 있게끔 됨 (xml의 editText 대신) 

- TextField는 기본 제공되는 스타일이 존재 , BaseTextField는 힌트나 자리표시자 등등을 제공하지 않는 기본 형식

- BaseTextField는 머트리얼 디자인 가이드를 따르지 않고 쓸때 사용하기를 권장

- TextField에서 사용자가 글을 입력시 표시되면 글 형식을 지정할 수 있음 (ex: 패스워드면 **)

- TextField에서 키보드 옵션 설정 가능 


참고

https://developer.android.com/develop/ui/compose/text/user-interactions?hl=ko


Jetpack Compose Text 1편

 이번에는 Compose의 텍스트에 관해 정리를 해봤습니다.

- Compose는 텍스트를 표시하고 사용자 입력을 처리하는 기본 구조인 BasicText 및 BasicTextField를 제공함

- 상위 수준에서 Compose는 Text 및 TextField를 제공하며 머티리얼 디자인를 따름

(위 Composable은 디자인과 분위기가 Android 사용자에게 적합하며 쉽게 커스텀 할 수 있는 옵션이 포함되어 있으므로 사용을 권장함)


1. 기본 Text 표시

텍스트를 표시하는 가장 기본적인 방법은 Text Composable을 사용하는 것입니다.

@Composable
fun SimpleText() {
    Text("Hello World")
    
    //문자열 리소스를 사용하는 예제
    Text(stringResource(R.string.hello_world))
}


2. Text Style 지정

Text Composable에는 콘텐츠의 스타일을 지정할 수 있는 여러 매개변수가 있습니다.

이 매개변수 중 하나를 설정할 때마다 전체 텍스트 값에 스타일이 적용됩니다. 

- color, fontSize, fontStyle, fontWeight, textAlign


언어 텍스트 방향에 따라 Text 이동하는 TextAlign.Start와 TextAlign.End를 사용하는 것을 권장함

(TextAlign.Left, TextAlign.Right 은 가급적 제한적 사용)

@Composable
fun SimpleText() {
    
    //텍스트 색상 변경
    Text("Hello World", color = Color.Blue)
    
    //텍스트 크기 변경
    Text("Hello World", fontSize = 30.sp)
    
    //텍스트를 기울임꼴로 설정
    Text("Hello World", fontStyle = FontStyle.Italic)
    
    //텍스트를 굵게 표시
    Text("Hello World", fontWeight = FontWeight.Bold)
    
    //텍스트 정렬
    Text("Hello World", textAlign = TextAlign.Center, modifier = Modifier.width(150.dp))
}


3. Text font

- Text의 fontFamily 매개변수를 사용하면 Composable에 사용되는 글꼴을 관리할 수 있음

- 기본적으로 Serif, Sans-serif, 고정폭 및 필기체 글꼴 모음이 포함

- fontFamily 속성을 사용하여 res/fonts 폴더에 정의된 맞춤 font로 작업할 수 있음



@Composable
fun SimpleText() {
    
    Text("Hello World", fontFamily = FontFamily.Serif)
    
    Text("Hello World", fontFamily = FontFamily.SansSerif)
}



- fontFamily를 Text 컴포저블에 전달할 수 있습니다.

- fontFamily에 수동으로 fontWeight를 설정하여 텍스트에 적합한 가중치를 선택할 수 있습니다.

val firaSansFamily = FontFamily(
    Font(R.font.firasans_light, FontWeight.Light),
    Font(R.font.firasans_regular, FontWeight.Normal),
    Font(R.font.firasans_italic, FontWeight.Normal, FontStyle.Italic),
    Font(R.font.firasans_medium, FontWeight.Medium),
    Font(R.font.firasans_bold, FontWeight.Bold)
)

Column {
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Light)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Normal)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Normal, fontStyle = FontStyle.Italic)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Medium)
    Text(..., fontFamily = firaSansFamily, fontWeight = FontWeight.Bold)
}



4. Text Composable 내의 여러 스타일 설정 

동일한 Text 컴포저블 내에서 여러 스타일을 설정하려면 AnnotatedString을 사용해야 합니다.

AnnotatedString은 다음 항목이 포함된 데이터 클래스입니다.


- Text 값

- SpanStyleRange의 List: 텍스트 값 내 위치 범위가 포함되는 인라인 스타일 지정과 동일함

- ParagraphStyleRange의 List: 텍스트 정렬, 텍스트 방향, 행 간격, 텍스트 들여쓰기 스타일 지정


TextStyle은 Text 컴포저블에서 사용되며

SpanStyle 및 ParagraphStyle은 AnnotatedString에서 사용됩니다.


SpanStyle과 ParagraphStyle의 차이점은

ParagraphStyle은 전체 단락에 적용할 수 있고,  SpanStyle은 문자 수준에서 적용할 수 있다는 것입니다.

@Composable
fun MultipleStylesInText() {
    Text(buildAnnotatedString {
        withStyle(style = SpanStyle(color = Color.Blue)) {
            append("H")
        }
        append("ello ")

        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, color = Color.Red)) {
            append("W")
        }
        append("orld")
    })
}



@Composable
fun ParagraphStyle() {
    Text(buildAnnotatedString {
        withStyle(style = ParagraphStyle(lineHeight = 30.sp)) {
        withStyle(style = SpanStyle(color = Color.Blue)) {
            append("Hello\n")
        }
        withStyle(style = SpanStyle(fontWeight = FontWeight.Bold,
            color = Color.Red)) {
            append("World\n")
        }
        append("Compose")
        }
    })
}




5. Text 최대 줄 수

- Text Composable에 표시되는 줄 수를 제한하려면 maxLines 매개변수를 설정

@Composable
fun LongText() {
    Text("hello ".repeat(50), maxLines = 2)
}


6. Text 오버플로

- 긴 텍스트를 제한할 때 표시된 텍스트가 잘린 경우에만 표시되는 텍스트 오버플로를 표시할 수 있음

- textOverflow 매개변수를 설정하면됨

@Composable
fun OverflowedText() {
    Text("Hello Compose ".repeat(50), maxLines = 2, overflow = TextOverflow.Ellipsis)
}


**정리

- Compose 의 Text Composable은 기본적으로 머티리얼 디자인 가이드에 따라 구현이 됨

- Text Composable의 매개변수로 스타일을 지정할 수 있음 크기, 색깔, 기울기, 정렬, 굵기,최대 줄수, 오버플로우 등등

- RTL, LTR 설정 컨텍스트에 따라 정렬이 자연적으로 달라지게 되어있음 (Left,Right 보다 Start,End 사용을 권장)

- font도 fontFamily parameter로 쉽게 설정가능, fontWeight parameter로 가중치 수동으로 설정가능

- Text Composable 내의 글자마다 설정을 다르게 하고 싶으면 AnnotatedString를 사용

=> ParagraphStyle은 전체단락 , SpanStyle은 문자 수준 적용


Jetpack Navigation + BottomNavigationBar 구현하기

1. Jetpack Navigation


사실 Jetpack Navigation에 대해서는 이전에도 한번 포스트를 쓴 적이 있다.

Navigation은 크게 3가지 요소로 이루어져 있다.


NavController

- 대상(즉, 앱의 화면) 간 이동을 담당하는데, 탐색 동작을 처리하는 역할

- 대상을 탐색하고 딥 링크를 처리하며 대상 백 스택을 관리하는 등의 여러 메소드를 제공


NavGraph

- 이동할 컴포저블 대상의 매핑을 담당

- 앱 내의 화면 연결을 보여주는 일종의 지도 역할을 한다.


NavHost

- 현재 탐색 대상(navigation destination)이 포함된 UI 요소

- NavGraph의 현재 대상을 표시하는 컨테이너 역할을 하는 컴포저블


2. BottomNavigationBar 구현하기

이제 위에서 설명한 Navigation과 Compose를 활용해 BottomNavigationBar를 구현해보자.

gradle에 라이브러리 추가

dependencies {
    implementation("androidx.navigation:navigation-compose:2.7.7")
}


Bottom Navigation으로 이동할 화면 생성

필자는 여기서 Home, Rating, Profile 3개의 화면을 만들어주었다.


@Composable
fun HomeScreen() {
    Text(text = "Home")
}

@Composable
fun RatingScreen() {
    Text(text = "Rating")
}

@Composable
fun ProfileScreen() {
    Text(text = "Profile")
}


BottomNavItem 생성.

Bottom Navigation으로 이동할 item 객체 클래스 BottomNavItem 클래스를 생성하고,

그 자식으로 Home, Rating, Profile 을 추가한다.

sealed class BottomNavItem(
    @StringRes val title: Int,
    @DrawableRes val icon: Int,
    val screenRoute: String
) {
    object Home : BottomNavItem(R.string.home, R.drawable.ic_home, NavScreens.Home.name)
    object Rating : BottomNavItem(R.string.rating, R.drawable.ic_rating, NavScreens.Rating.name)
    object Profile : BottomNavItem(R.string.profile, R.drawable.ic_profile, NavScreens.Profile.name)
}


NavHost, NavGraph 생성.

NavHost 함수의 NavGraphBuilder 를 통해서 NavGraph를 생성할 수 있다.

@Composable
private fun MyNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController,
    startDestination: String
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        ...

        composable(route = NavScreens.Home.name) {
            HomeScreen(
                onSearchClicked = { navController.navigate(NavScreens.Search.name) }
            )
        }

        composable(route = NavScreens.Rating.name) {
            RatingScreen()
        }

        composable(route = NavScreens.Profile.name) {
            ProfileScreen()
        }

        composable(route = NavScreens.Search.name) {
            SearchScreen()
        }
    }
}


Bottom Navigation 생성
@Composable
private fun MyBottomNavigation(
    modifier: Modifier = Modifier,
    navController: NavHostController
) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    val items = listOf(
        BottomNavItem.Home,
        BottomNavItem.Rating,
        BottomNavItem.Profile
    )

    NavigationBar {
        items.forEach { item ->
            NavigationBarItem(
                selected = currentRoute == item.screenRoute,
                label = {
                    Text(
                        text = stringResource(id = item.title),
                        style = TextStyle(
                            fontSize = 12.sp
                        )
                    )
                },
                icon = {
                    Icon(
                        painter = painterResource(id = item.icon),
                        contentDescription = stringResource(id = item.title)
                    )
                },
                onClick = {
                    navController.navigate(item.screenRoute) {
                        navController.graph.startDestinationRoute?.let {
                            popUpTo(it) { saveState = true }
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                },
            )
        }
    }

}


BottomNavigationBar를 그리는 앱 화면 구성

@Composable
fun MyApp() {
    val navController = rememberNavController()

    Scaffold(
        modifier = Modifier.fillMaxSize(),
        topBar = {
            ...
        },
        bottomBar = {
            MyBottomNavigation(navController = navController)
        }
    ) {
        Box(modifier = Modifier.padding(it)) {
            MyNavHost(
                navController = navController,
                startDestination = NavScreens.Home.name
            )
        }
    }
}



Android Compose Animation

나타남 /  사라짐 애니메이션 AnimatedVisibility 의 enter 및 exit 매개변수를 사용하면 컴포저블이 표시되고 사라질 때의 동작을 구성할 수 있습니다. 자세한 내용은  전체 문서 를 참고하세요. 컴포저블의 가시성을 애니메이션화하는 ...