Intro
야심차게 준비한 앱을 마켓에 출시한지 며칠 뒤, 서버 비용 및 API 서버 이슈로 인해 서버가 내려가는 이슈를 겪었습니다.
그다지 크리티컬한 이슈는 아니었지만, 백엔드 담당자분께서 내리신 결론이었기에 그저 따르기로 했습니다.
백엔드 담당자분의 요청으로 굉장히 짧은 시간 안에 앱을 구현, 출시했습니다. 그 과정에서 발생한 기술 부채 및 각종 하드코딩된 부분들을 해결하고 싶었으나, 애초에 서버가 내려가서 의미 없는 작업이 되고 말았습니다.
디자인을 담당해주셨던 분과 긴 시간 대화를 나눴고, 끝내 저희가 내린 결론은 '백엔드를 배제하고 앱을 새로 만들어서 재출시하자' 였습니다.
주 3회에 이르는 잦은 회의와 빠듯한 일정으로 인해 디자인이나 앱이나 모두 각 담당자의 마음에 쏙 들지는 않았던 것 같습니다. (물론 디자인은 최고였습니다 ^^d) 서버 이슈로 작동이 안 되는 건 덤이구요.
그리하여, 백엔드 개발자분으로부터 로직에 대한 설명을 받고, 새로운 API 를 활용하여 새롭게 개발하고있습니다. 해당 포스트에서는 개발 과정 중 Expandable Layout 을 구현하면서 배우게 된 지식을 기록하고자합니다.
Expandable Layout 을 구현하는 세 가지 방법.
여러가지 방법이 있습니다. 제일 쉽고 빠른 방법은, 무엇보다 라이브러리를 사용하는 방식입니다.
개인적으로 존경하는 sykdoves (엄재웅) 님의 ExpandableLayout 이 유명한 것 같습니다.
기존 앱에서도 해당 라이브러리를 사용했는데, 해당 부분은 제가 아닌 다른 분께서 작업을 하셨습니다.
라이브러리는 필요한 기능을 굉장히 쉽고 빠르게 구현할 수 있습니다.
복잡한 코드가 필요하지않고 대부분 XML 에서 구현이 가능합니다.
이 말은 곧, 스터디의 목적이 없다면 라이브러리를 사용하는 것이 시간을 적게 소모하므로 효율이 높음을 의미합니다.
하지만 저는 사이드 프로젝트의 근본적 목적인 개인의 성장을 고려하여, View 에 대한 라이브러리 사용은 최대한 지양하고 있습니다. 직접 구현하면서 얻게 되는 지식의 질이 상당히 좋기 때문입니다.
다음은 Animation Resource (ValueAnimator 등) 을 이용하는 방식입니다.
제가 가장 먼저 시도했던 방식입니다. res 경로에 anim 디렉토리를 추가하고, 내부에 Animation Resource 를 구현, Activity 에서 호출하여 사용하는 방식입니다.
// R.anim.dropdown
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:fillAfter="true">
<scale
android:duration="@android:integer/config_mediumAnimTime"
android:fromXScale="1.0"
android:fromYScale="0.0"
android:toXScale="1.0"
android:toYScale="1.0" />
</set>
// MainActivity 에서 Animation 을 실행하는 코드
binding.dropdownMenu.startAnimation(AnimationUtils.loadAnimation(this@MainActivity, R.anim.dropdown))
근데 구현을 이렇게하면, 뷰가 위에서부터 서서히 나타나는게 아니라, 위로 압축되어있다가 팽창하는 느낌으로 보입니다.
그래서 이 방법은 패스하기로 했습니다. 아마 Animation Resource 를 사용해서 제가 원하는 액션을 보여줄 수는 있을 것 같은데, 이에 대해서는 스터디를 따로 해보아야 할 것 같습니다.
제가 진행했던 방식은 TransitionManager Class 를 이용하는 방식입니다.
TransitionManager 의 동작은 View 에 대한 스냅샷을 찍는 것이라 생각하면 이해하기 편합니다.
변형 전의 View Widget 에 대한 스냅을 찍은 뒤, 해당 View 에 대한 속성을 변경시키고 requestLayout() 메서드를 호출하면 자연스러운 변화를 보여줍니다.
해당 방식을 진행하면서도 트러블이 있었습니다.
숨기고자 하는 View 의 visiblity 를 조정하여 확장 및 축소하는 방식으로 진행을 했었는데, 그렇게 구현을 하니,
visibility gone -> visible 의 경우 곧바로 확장되면서 원하지 않던 내부 View 들의 alpha 값 변화가 있었고 (0 -> 1), visibility visible -> gone 의 경우 Layout 내에 구현한 View 들의 alpha 값이 서서히 1 에서 0 으로 변한 뒤, 내부가 텅 빈 상태에서 Layout 만 축소될 뿐이었습니다. 그야말로 총체적 난국입니다.
그리하여, dp 값을 구해서 Layout 을 축소시키기로 결정하였습니다.
Layout 내부에 View 들을 모두 구현해두고, 초기화 시 보여질 부분만큼만 height 를 지정하는 것입니다. 그렇게 한 뒤, LayoutParams 의 height 를 WRAP_CONTENT 로 바꿔주는 방식으로 진행하면 되리라 판단하였고, 구현해보니 정상 작동했습니다. 이하는 코드입니다.
// activity_main.xml
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="30dp"
android:layout_margin="10dp"
android:background="@drawable/radius"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btn">
<View
android:id="@+id/dropdown"
android:layout_width="16dp"
android:layout_height="16dp"
android:backgroundTint="@color/green"
android:layout_margin="7dp"
android:background="@drawable/dropdown"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
... (중략)
</androidx.constraintlayout.widget.ConstraintLayout>
ConstraintLayout 의 height 가 30으로 되어있습니다. 초기화시 보여질 부분입니다.
// 전역 변수
private val collapsedHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30F, resources.displayMetrics).toInt()
private val state = 0
//
private fun expand() {
TransitionManager.beginDelayedTransition(parentLayout)
state = if (state == 0) {
playAnimation(dropdown, R.anim.spin)
parentLayout.layoutParams.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
1
} else {
playAnimation(dropdown, R.anim.reverse)
parentLayout.layoutParams.height = collapsedHeight
0
}
parentLayout.requestLayout()
}
private fun playAnimation(view: View, anim: Int) {
view.startAnimation(AnimationUtils.loadAnimation(this@MainActivity, anim)
}
TransitionManager 에서 기존의 View 를 스냅, 이후 코드를 통해 View 의 속성을 변경, requestLayout() 메서드를 통해 다시 그리는 프로세스를 거칩니다.
항상 시도하기 꺼려했던 Animation 과 View 에 대해 한 걸음 더 가까이 접근할 수 있었습니다.
사실 모바일 앱 개발의 경우는 백엔드보다 프론트엔드의 개념이 더 커서, View 에 대한 높은 이해도와, 이를 잘 다룰 수 있는 능력을 함양하는 것이 중요하다고 생각합니다.
'Android > Tech' 카테고리의 다른 글
[Dagger-Hilt] ActivityScope 와 ActivityRetainedScope 의 차이 (0) | 2022.10.21 |
---|---|
Coroutine Dispatcher 란 무엇인가? (0) | 2022.10.19 |
Fragment Lifecycle 과 UX (feat.Navigation Component) (0) | 2022.06.03 |
LiveData 를 선언하는 방법 (get Keyword) (0) | 2022.05.08 |
자주 쓰는 Intent Flag (0) | 2022.04.21 |