레이블이 android인 게시물을 표시합니다. 모든 게시물 표시
레이블이 android인 게시물을 표시합니다. 모든 게시물 표시

2022년 1월 21일 금요일

[요약] Android Touch System — Part 1: Touch Functions and the View Hierarchy

https://proandroiddev.com/android-touch-system-part-1-touch-functions-and-the-view-hierarchy-1f6526e55d78

Android Touch System — Part 1: Touch Functions and the View Hierarchy


터치 스크린상에서의 모든 움직임은 MotionEvent로 알려진다.

MotionEvent는 다음의 값들을 가진다.

action : 수행한 action의 타입
x : 터치한 x좌표, view에 상대적인 위치
y : 터치한 y좌표,  view에 상대적인 위치
rawX : 터치한 절대 x좌표, 디바이스 화면에 상대적인 위치
rawY : 터치한 절대 y좌표, 디바이스 화면에 상대적인 위치
eventTime : 이벤트가 발생한 시간, SystemClock.uptimeMilllis()

안드로이드 화면은 좌상단이 (0, 0)이고 우하단이 (maxX, maxY)이다.

action은 다음의 값을 가진다.


ACTION_DOWN : 터치가 처음으로 일어났을 때
ACTION_UP : 터치를 화면에서 떼었을 때
ACTION_MOVE : ACTION_DOWN과 ACTION_UP 사이에 터치가 이동할 때
ACTION_CANCEL : 현재 터치가 취소될 때. parent view가 child view의 이벤트를 인터셉트할 때 발생한다.

이벤트의 흐름


motion event가 발생하면 root view(예: Activity)로부터 중간에 인터셉트당하지 않으면 가장 하위의 view까지 내려간다. 내려갈 때, view의 dispatchTouchEvent()가 호출된다. 여기 안에서 onInterceptTouchEvent()가 호출되는데 false를 리턴하면 아래로 계속 내려가지만 true를 리턴하면 더이상 아래로 내려가지 않는다.(이벤트는 여기서 소비된다.) 이벤트가 leaf view까지 내려가거나 onInterceptTouchEvent()가 true를 리턴해서 더이상 아래로 내려가지 않으면 다시 위로 올라가면서 onTouchEvent()가 호출된다. onTouchEvent()가 true를 리턴하면 이벤트는 여기서 멈춘다.

dispatchTouchEvent()

View.dispatchTouchEvent : view는 children을 갖지 않는다. 따라서, 구현은 간단하다. onTouchEvent()를 호출하고, touch listener를 view에 설정한다. 이 리스너중 하나라도 true를 리턴하면 true를 리턴한다.
custom view는 dispatchTouchEvent 보다는 onTouchEvent를 override 하는 것이 좋다.

ViewGroup.dispatchTouchEvent : onInterceptTouchEvent()를 호출한다. 여기서 false를 리턴하면 추가된 반대의 순서로 child view들을 순회한다. 이중 터치가 child view의 안에서 발생한 것이면 child.dispatchTouchEvent()를 호출한다. 여기서 false를 리턴하면 다음 child에서 child.dispatchTouchEvent()를 호출한다.
ViewGroup은 dispatchTouchEvent 보다는 onInterceptTouchEvent를 override 하는 것이 좋다.

ScrollView.dispatchTouchEvent : ViewGroup과 같다.

Activity.dispatchTouchEvent : 자식들의 dispatchTouchEvent()를 호출한다. 

onInterceptTouchEvent()


View.onInterceptTouchEvent : 없다.

ViewGroup.onInterceptTouchEvent : 기본 구현은 false를 리턴한다. 이 함수를 override 하는 주 목적은 특정한 타입의 터치 이벤트만 다루고 나머지는 자식들이 다루도록 하기 위함이다. 예를 들어 ScrollView는 스크롤은 직접 다루고, 클릭 같은 것은 자식이 다루도록 한다.

ScrollView.onIntreceptTouchEvent : 이벤트가 ACTION_MOVE이고, velocity가 충분하면 true를 리ㅣ턴한다. 그리고 자식들은 ACTION_CANCELLEDD를 받게 된다. 또한 requestDisallowInterceptTouchEvent를 호출한다.

Activity.onInterceptTouchEvent : 없다.

onTouchEvent()


View.onTouchEvent : view가 clickable 하면 기본은 true를 리턴한다. overriding할거면 super.onTouchEvent()를 호출해주는 것이 좋다. 또는 click gesture만 다룰거라면 performClick()를 override하는 것이 좋다.

ViewGroup.onTouchEvent : View의 onTouchEvent와 같다.

ScrollView.onTouchEvent : event의 정보를 통해 얼마나 스크롤했는지를 알아내서 스크롤을 수행한다. 스크롤과 관련된 애니메이션도 다룬다. 또한 requestDisallowInterceptTouchEvent()를 호출한다.

Activity.onTouchEventv : 기본 구현은 항상 false를 리턴한다.

requestDisallowInterceptTouchEvent()

ViewParent 인터페이스의 함수. parent나 ancestor view가 터치 이벤트를 인터셉트하지 못하도록 할 때 사용한다.

2020년 4월 11일 토요일

Cancellation and Exceptions in Coroutines 요약

1. Coroutines: First things first

- CoroutineScope :  launch나 async로 실행행된 코루틴을 참조. Job이 있는 경우 scope.cancel()을 통해 코루틴을 취소할 수 있다. 이때 관련된 모든 코루틴들(children)이 취소된다.

- Job : 코루틴에의 핸들. 코루틴을 생성하면 리턴되는 값. 코루틴의 lifecycle을 관리한다. 특정 코루틴만 취소하고 싶은 경우에는 코루틴이 리턴하는 Job의 cancel()을 통해 그 코루틴만 취소할 수 있다.

- CoroutineContext : Job, CoroutineDispatcher, CoroutineName, CoroutineExceptionHandler를 통해 코루틴의 행동을 정의하는 context로서 Job을 통해 코루틴의 lifecycle을 관리하고 CoroutineDispatcher를 통해 어떤 쓰레드에서 작업을 실행할지 지정하고 CoroutineName은 코루틴의 이름을 의미하고 CoroutineExceptionHandler를 통해 uncaught exception을 처리한다.

- Job lifecycle : 정상적인 경우 다음의 상태를 가진다. New -> Active -> Completing -> Completed. Active나 Completing에서 cancel이나 fail이 발생하면 Cancelling 상태로 변경되고, 이 Job과 관련된 모든 코루틴들의 작업이 끝나면 Cancelled로 변경된다.

- Parent context = Defaults + inherited CoutineContext + arguments
(Defaults : CoroutineDispatcher=Dispatchers.Default, CoroutineName="coroutine")

- New coroutine context = parent CoroutineContext + Job()

2. Cancellation in coroutines

- Job을 취소할 때 사용하는 함수인 cancel()에는 parameter로 CancellationException을 받을 수 있다. 취소에 대한 좀 더 자세한 정보를 알리고 싶을 때 사용한다.
Job이 취소되면  job은 자신의 parent에게 exception을 알린다. parent는 이 exception이 CancellationException이면 무시한다.

cancel() 함수를 호출한다고 해서 코루틴이 하던 작업이 바로 취소되는 것은 아니다.(cooperative) 따라서, 작업이 끝날때까지 무한 루프를 도는 코루틴이라면 중간에 취소 여부를 확인해주지 않으면 작업이 끝날때까지 동작하게 된다.

- 중간에 취소여부를 확인하기위해 다음의 4가지 방법을 사용할 수 있다. suspend function, job.isActive, ensureActive(), yield()
모든 suspend 함수는 cancellable하기 때문에 이 지점에서 취소 여부에 따라 중단될 수 있다.
if 문에서 isActive를 통해 상태를 확인할 수 있다. 이를 편하게 해주는 ensureActive() 함수를 제공하고 있다. yield는 코루틴의 상태가 cancelled나 completed이면 CancellationException을 throow한다.

- 코루틴으로부터 결과를 기다리는 두가지 방법 : Job.join(), Deferred.await()
Job.join() : 코루틴의 작업이 끝날때까지 기다린다.
Deferred.await() : async { ... }의 리턴에 대해 await를 호출하면 async의 작업이 끝날때까지 기다린다. cancell된 deferred에 대해 await를 호출하면 JobCancellatingException을 throw한다.

- 코루틴이 취소되었을 때 특정한 action을 실행하는 방법
- isActive를 통해 작업을 끝내고 clean up을 진행한다.
- 작업이 취소되면 CancellationException이 호출되므로 try catch finally를 사용한다.
=> clean up 할 때 suspend 함수를 사용하면 안된다.(suspend 함수는 cancelled 상태에서는 실행되지 않는다. 굳이 하고 싶으면 withContext(NonCancellable)을 사용한다.)

3. Exceptions in Coroutines

- Exception 전달 경로
child coroutine에서 exception이 발생하면 이 정보가 parent에게 전달된다. -> parent는 모든 children coroutine을 취소한다. -> exception가 다시 상위(parent의 parent)로 전달된다.

- 모든 children이 취소되지 않고 나 자신만 취소되게 하려면 SupervisorJob을 사용한다. 이 경우 취소된 child coroutine만 취소되고 다른 child들은 취소되지 않는다.

- Exception 처리하기

-- launch : exception이 발생하면 바로 throw된다. 따라서, 코드에서 try catch를 사용한다.
-- async

--- root coroutine일 경우에는 exception이 바로 throw 되지 않고 나중에 await를 실행할 때 throw 된다. 따라서, await() 주위에서 try catch를 사용한다. 또한 이때 SupervisorJob을 사용행야 한다. Job을 사용하면 exception이 parent로 전달되어 catch가 동작하지 않게 된다.
--- 다른 코루틴 안에서 async가 실행될 경우에는 exception이 바로 throw 된다.

- CoroutineExceptionHandler의 실행 조건(둘 다 성립해야 한다.)
-- exception을 throw하는 코루틴에서 실행된다.(work with launch, not async)
-- root coroutine이거나 CoroutineScope의 CoroutineContext안에 있는 경우

4. Coroutines & Patterns for work that should't be cancelled

- Coroutine vs WorkManager
process가 있는 동안만 동작해야 하는 경우에는 Coroutine을 사용하고 process와 상관없이 background에서 동작해야 하는 경우에는 WorkManager를 사용한다.

- 취소되지 않아야 하는 작업의 경우를 위해 Application 레벨에서의 CoroutineScope을 만들어서 사용한다.
- GlobalScope, ProcessLifecycleOwner, NonCancellable은 사용하지 않는다.

2019년 12월 31일 화요일

Complex UI/Animations on Android 정리

https://proandroiddev.com/complex-ui-animation-on-android-8f7a46f4aec4

1. 스크롤 할 때 툴바에 애니메이션 효과 주기

AppBarLayout의 behavior에 내가 사용하려는 애니메이션 효과를 가지는 커스텀 CoordinatorLayout behavior을 지정해준다.

아래와 같은 클래스를 만들고 onStartNestedScroll(스크롤이 시작할 때 호출되는 함수)과 onNestedScroll(스크롤중일 때 호출되는 함수)을 구현한다.

class ToolbarBehavior : CoordinatorLayout behavior<AppBaLayoutr>() { ... }

- onStartNestedScroll

위 아래로의 스크롤(vertical)일 때 true를 리턴한다.

- onNestedScroll

dyConsumed의 값이 때라 scroll up인지 down인지 파악한다. dyConsumed가 양수이면 scroll down이고, dyConsumed가 음수이면 scroll up임을 의미한다.

height를 변경하고 나면 requestLayout을 호출해 주어야 한다.
위치를 바꿀 때 translationX와 translationY를 사용한다.
scale을 적용할 때 scaleX와 scaleY를 사용한다.

추가로 ToolbarBehavior의 효과를 적용할 때 관련 뷰들을 참조하고 있어야 한다. 따라서, onStartNestedScroll이나 onNestedScroll이 호출되기 전에 이러한 관련 값(Views, height, 최소 height등)들을 미리 설정해 놓고 있어야 한다.

2. RecyclerView의 item의 height 변경할 때 애니메이션 효과 주기

아이템의 평상시 height와 커졌을 때의 height를 계산해놓고 있다가 사용자가 아이템을 클릭할 때 이에 맞추어 애니메이션 효과를 주는 것이 필요하다.

- Adapter의 onViewAttachedToWindow에서 height를 계산한다.

onViewAttachedToWindow는 Adapter에서 만든 뷰가 RecyclerView에 연결될 때 호출되는 함수이다.

- 평상시 height와 커졌을 때의 height를 계산하는 방법은 다음과 같다.

doOnLayout에서 평상시 뷰의 height를 계사한다. -> expanded view를 visible로 설정한다. -> doOnNextLayout에서 expanded view의 height를 계산한다. -> expanded view를 hide한다.

위와 같이 하면 사용자에게 뷰가 커졌다 작아졌다 하는 것을 보여주지 않고 각각의 height를 알 수 있게 된다.(이유는? : 아마도 onDraw가 불리기 전에 관련 layout이 실행되는 동안 height를 알아내서 사용자에게는 안보이게 되는 것이 아닐까 추측. 정확하게는 모르겠네요.) 여기서 한가지 더 주의할 것은 expanded view의 hide를 호출하는 부분을 expandedView.post { ... } 안(또는 doOnPreDraw에서)에서 해주어야 한다는 점이다.(expandedView.visible = false는 layout이 일어나도록 하지 않는다.)(post나 doOnPreDraw를 사용하면 onDraw가 불리기 전에 layout이 다시 일어나게 되는 걸까요? 이부분도 잘 모르겠군요.)

아이템을 클릭하면 이전에 커진 뷰가 있다면 원래 크기로 돌려야 하고 새로 클릭된 뷰를 크기를 키워야 한다. RecyclerView의 findViewHolderForAdapterPosition를 통해 이전에 커진 뷰를 찾을 수 있다.

ValueAnimator를 사용해서 점진적으로 커지는 애니메이션 효과를 줄 수 있다.

3. ViewPager2의 위치가 변경될 때 관련 탭의 위치도 같이 변경하기

ViewPager의 onPageScrolled에서 position과 positionOffset을 받아서 탭의 위치를 변경한다.(이 문서에서는 RecyclerView의 scrollBy를 사용해서 탭의 위치를 변경했는데 RecyclerView의 layout manager가 LinearLayoutManager라면 LinearLayoutManager의 scrollToPositionWithOffset를 사용하는 방법도 있다. 이게 더 간단한 방법인듯 하다.)

4. Filter sheet animation

네 단계로 나누어진 애니메이션 효과를 준다. 각각의 애니메이션이 동작하는 순서를 위해 AnimationSet을 사용한다.

a. Fab 버튼의 path animation
b. RecyclerView의 아이템들을 shrink하는 scale down animation
c. Fab 버튼이 Filter sheet로 변경되는 animation
d. 최종 뷰들 보여주기

각각을 자세히 살펴보자.

a. Fab 버튼의 path animation

현재 위치에서 화면의 중간으로 이동한다.
애니메이션을 위해 ValueAnimator를 사용하는데 이때의 progress를 ArcMetric를 사용해서 변경된 형태로 뷰의 x와 y에 적용한다.

b. RecyclerView의 아이템들을 shrink하는 scale down animation

ValueAnimator를 써서 현재 화면에 보이는 아이템들을 scale down 한다.

c. Fab 버튼이 Filter sheet로 변경되는 animation

Fab 버튼의 크기를 최종 뷰의 크기로 변경한다. 이때 0 ~ 0.8 까지는 원의 형태로 커지다가 0.8 ~ 1 로는 사각형으로 커지게 한다.

d. 최종 뷰들 보여주기

개별 뷰들을 화면에 보여준다.

5. Filtering animation

여섯 단계로 나누어진 애니메이션이다.

a. Fab Collapse Animation
b. Bottom Bar Animation
c. Tabs and ViewPager fade out animation
d. Close Icon rotate animation to simulate loading

rotation값을 변경한다.

e. Fab Arc Path animation
f. RecyclerView items scale down animation

2019년 11월 21일 목요일

Composition over Inheritance: Adding a Material Speed Dial to a Floating Action Button 요약

https://proandroiddev.com/composition-over-inheritance-adding-a-material-speed-dial-to-a-floating-action-button-1e646995ab49

FAB 버튼을 누르면 추가 버튼이 나오는 UI를 생각해보자. 어떻게 구현하는 것이 좋을까? Custom View를 만드는 방법을 생각해 볼
수 있다. 이렇게 구현된 많은 오픈소르 라이브러리들이 있다. 그런데 이 라이브러리들의 동작이 내가 원하는 것과 다르면 어떻게
해야 할까? 클래스를 상속받아서 내가 원하는 형태로 변경하면 될까? 이렇게 하려면 상속이 가능해야 하고 또한 내가 원하는
함수를 override 해서 처리할 수 있게 되어 있어야 한다. 그렇지 않다면 힘들것이다.

View와 Behavior를 분리해서 작성하는 건 어떨까?(Composition over inheritance)

1. FAB를 클릭했을 때 보여주는 PopupWindow 작성

1.1. anchor view의 위치에 따라 PopupWindow의 위치 지정: doOnLayout
1.2. PopupWindow는 FragmeLayout으로 구성하고 addView를 통해 내부에 필요한 view를 넣는다 -> LinearLayout으로 구성
1.3. LinearLayout의 아래에서 부터 item을 넣어야 하므로 MIRROR를 사용
1.4. LayoutAnimationController를 사용하여 item을 보여줄 때 애니메이션을 보여준다.
1.5. ripple 효과를 위해 RippleDrawable을 만들고 button의 background에 설정

2. FAB를 클릭시 동작 지정

2.1 버튼을 회전시키는 spring animation
2.2 1에서 만든 PopupWindow 보이기
2.3 PopupWindow의 dismiss시 동작 설정

위처럼 1과 2의 형태로 분리하면 어떤 View를 사용해서라도 추가 버튼이 나오는 형태를 구현할 수 있다.


2019년 11월 14일 목요일

Injection into Android Component’s Constructors is real 요약

https://proandroiddev.com/inject-into-android-component-constructor-4f5ddd27d06

안드로이드는 Activity와 Fragment를 직접 생성하지 않고 OS가 생성을 해줍니다. 그러다보니 생성시 필요한 값이 있을 때
생성자에 넣어줄 수는 없고 Activity.onCreate()나 Fragment.onAttach()에 넣어주게 되죠. 그러다보니 lateinit을 사용하게
됩니다.
그렇게 큰 불편함은 없지만 이와이면 생성자에서 값을 생성할 수 있으면 좋을텐데요. Activity의 경우에는
AppComponentFactory를 사용해서, Fragment의 경우에는 FragmentFactory를 사용해서 생성자에 값을 넣어줄 수 있습니다.
다만, AppComponentFactory는 안드로이드 9 부터 사용이 가능하다는 단점이 있네요. 하지만 요즘 추세는 Single activity이니까
큰 문제 없겠죠?

1. FragmentFactory를 상속한 InjectFragmentFactory를 만든다.
2. Application의 registerActivityLifecycleCallbacks에 SetFragmentFactoryActivityCallbac를 등록한다.
3. SetFragmentFactoryActivityCallback는 EmptyActivityLifecycleCallbacks를 상속해서 만드는데 onActivityCreated에서
fragmentManager에 SetFragmentFactoryFragmentCallback를 등록하고 FragmentFactory를 설정한다.
4. SetFragmentFactoryFragmentCallback는 FragmentManager.FragmentLifecycleCallbacks를 상속해서 만드는데 child
fragment에도 FragmentFactory를 설정해준다.

여기까지가 FragmentFactory를 적용하기 위한 설정이고 다음에는 Dagger를 통해 FragmentFactory에 대한 DI 적용을 한다.

5. Component와 Module 및 Multi bind를 생성한다: AppComponent, AppModule, ComponentProvidersModule, FragmentBindsModule, FragmentKey

Dagger를 통한 생성은 multi bind를 공부해 봐야 할듯 하군요.

https://dagger.dev/multibindings

2019년 10월 26일 토요일

Bezier Curve 공부

Drawing Bezier Curves like in Google Material Rally

How I drew custom shapes in bottom bar

How I make CustomBottomSheet

Drawing bezier curves

Cubic Bezier curve : start point와 end point 그리고 두개의 control point로 구성된다.

Bezier curve를 그리기 위해 아래의 세단계를 취한다.

1. data point 지정
2. connection point 지정
3. point들을 연결하는 path를 정하고 canvas에 그린다. 이때 사용하는 함수는 cubicTo이다.

2019년 10월 25일 금요일

uCrop, the Image Cropping Library for Android: Look under the Hood of Our Masterpiece 를 읽고

https://yalantis.com/blog/how-we-created-ucrop-our-own-image-cropping-library-for-android/

[세 파트로 나누어서 구현]

- TransformImageView

ImageView를 상속
소스로부터 이미지 로드
matrix transform(translate, scale, rotate)하기

- CropImageView

crop boundary와 grid 그리기
이미지 이동시 crop boundary에 빈 영역이 생기지 않도록 이미지 위치시키기
지정한 룰(minimum scale, maximum scale 등)에 따라 matrix transform하는 함수 추가
zoom in/out
이미지 crop 하기

- GestureImageView

사용자 제스쳐(zoom, scroll, rotate gestures)

1. TransformImageView

크기가 큰 이미지의 경우 그대로 폰에 로드하면 메모리 문제가 발생할 수 있으므로 이미지를 샘플링해서 로드하는 것이 필요하다. 샘플링에 필요한 값은 screen diagonal로 정한다.
(BitmapFactory.options의 inJustDecodeBounds 를 사용해서 이미지를 메모리로 로드하지 않고 이미지의 정보만을 가져올 수 있다.)

2. CropImageView

이미지가 crop boundary를 전부 채우고 있는지 아닌지 확인 : crop bound의 네 코너가 이미지 안에 들어오는지 아닌지 확인
이미지가 crop boundary 안에 들어오도록 transform 하기 : 이미지의 센터와 crop bound의 센터 사이의 거리 계산. 그리고 이미지가 crop boundary 를 채우고 있지 못하면 scale을 한다.
이미지 crop 하기 :

3. GestureImageView

Android SDK에서 제공하는 GestureDetector와 ScaleGestureDetector를 사용하여 제스쳐 구현.
Rotation Gesture는 제공하지 않기 때문에 직접 구현




2019년 10월 23일 수요일

Testing Camera and Gallery Intents with Espresso 를 읽고

https://proandroiddev.com/testing-camera-and-galley-intents-with-espresso-218eb9f59da9

사진을 찍거나 갤러리에 접근하려면 권한이 필요하기 때문에 관련 작업을 하기전에 안드로이드에 권한을 요청해야 합니다. 앱의 코드에서는 권한을 요청하면 권한이 없는 경우 다이얼로그가 뜨고 여기서 OK를 하면 다음으로 진행이 되게 됩니다.
그러면, 테스트 코드에서는 어떻게 하면 될까요? 아래와 같은 코드를 테스트쪽에 넣어주면 테스트시 권한을 자동으로 부여합니다.

@get:Rule
var mRuntimePermissionRule = GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)

Activity parameter는 다음의 코드로 가져올 수 있습니다.

@get:Rule
var mActivityTestRule = IntentsTestRule(MyActivity::class.java)

- 갤러리로부터 이미지 가져와서 ImageView에 넣는 코드 테스트하기

a. 가장 먼저 image를 로컬에 저장합니다.

b. 그리고 Intent.ACTION_CHOOSER action이 발생하면 응답으로 줄 ActivityResult를 등록해 줍니다.

intending(hasAction(Intent.ACTION_CHOOSER)).respondWith(imgGalleryResult)

c. ActivityResult는 아래와 같은 코드로 만들 수 있습니다.

Instrumentation.ActivityResult(Activity.RESULT_OK, resultData)

d. View에서의 동작을 지정해줍니다.

onView(withId(R.id.gallery)).perform(click())

onView(withId(R.id.image_viewer)).check(matches(hasImageSet()))

R.id.gallery에 click 이벤트가 발생하면 image_viewer에 hasImageSet에 일치하는지 확인하는 코드입니다.

hasImageSet은 BoundedMatcher를 상속해서 matchesSafely에 확인하고 싶은 것을 체크합니다.

관련 구글 코드 : https://github.com/android/testing-samples/blob/master/ui/espresso/IntentsAdvancedSample/app/src/androidTest/java/com/example/android/testing/espresso/intents/AdvancedSample/ImageViewerActivityTest.java

2019년 10월 16일 수요일

Testing two consecutive LiveData emissions in Coroutines 를 읽고

https://medium.com/androiddevelopers/testing-two-consecutive-livedata-emissions-in-coroutines-5680b693cbf8

LiveData에서의 데이타 발생을 테스트할 때 발생했던 문제를 이야기 합니다.

예를 들어 네트워크로부터 데이타를 가져오는데 시간이 걸리는 경우 우선은 화면에 더미? 또는 간단한 데이타를 보여주고 실제 데이타를 얻어오면 다시 화면을 업데이트 하게 할 수 있습니다.
이에 대한 테스트 코드를 만들어서 테스트를 한다면 하나의 LiveData를 만들어서 emit이 두번 일어나는지를 확인해 보게 될겁니다. 그런데 LiveData는 최종 데이타만을 가지고 있기 때문에 Dispatcher.UnConfined의 코루틴으로 테스트를 했더니 항상 첫번째 emit은 일어나지 않고 두번째 emit만 일어나서 테스트가 실패했다고 합니다.

이에 대한 해결책으로 TestCoroutineDispatcher의 pauseDispatcher와 resumeDispatcher를 사용해서 두번 emit이 일어날 수 있도록 변경했다고 합니다. TestCoroutineDispatcher에 코루틴의 동작을 제어(중단 및 다시 실행)할 수 있는 함수가 있어서 이를 통해 해결이 가능했네요.

이외에 liveData coroutines builder를 사용해서도 해결할 수 있다고 합니다. 이 builder를 쓰면 데이타를 전달할 때 emit() 함수를 사용하게 되는데 이 경우 데이타 전달이 확실히 일어나게 되는거 같네요.

Good Practices

- 코루틴을 사용할 때 Dispatcher는 꼭 Dependency Injection의 형태로 사용해라.

- Dispatchers.UnConfined 대신에 TestCoroutineDispatcher를 사용해라.


ViewModel with SavedStateHandle

ViewModels: Persistence, onSaveInstanceState(), Restoring UI State and Loaders

Saving UI state with ViewModel SavedState and Dagger

ViewModels: State persistence — SavedState

A Deep Dive into Extensible State Saving

요즘 안드로이드 개발에서는 ViewModel을 많이 사용합니다. Configuration 변경에 대한 처리를 위해 사용하기도 하겠지만 저같은 경우는 MVVM 아키텍쳐 적용에 따라 ViewModel을 사용하고 있습니다.
안드로이드에서는 보통은 view에 관련된 data를 저장하고 싶을 때 onSaveInstanceState를 통해 저장합니다. 그리고 나서 나중에 Activity나 Fragment가 생성될 때 savedInstanceState를 확인해서 필요한 값이 있으면 꺼내서 사용하죠.
그런데 MVVM을 사용하게 되면서 data를 ViewModel쪽으로 이동하게 되고 이럴경우 data를 어떻게 유지할 것인가가 이슈가 됩니다.
이에따라 구글에서는 이에 관련된 lifecycle-viewmodel-savedstate 라는 모듈을 내놓았습니다. 이 모듈에서는 어떻게 ViewModel과 Activity/Fragment 사이에서 state(data)를 유지하는지 살펴보겠습니다.

크게는 Activity/Fragment에서의 처리와 ViewModel에서의 처리 이렇게 둘로 나누어서 볼 수 있습니다.

Activity/Fragment의 경우는 기본 컨셉은 간단합니다. 종료시 ViewModel에서 제공하는 state를 저장하고 다시 시작할 때 이렇게 저장된 state를 꺼내와서 ViewModel에 전달해 줍니다.

Activity/Fragment -> SavedStateRegistryController -> SavedStateRegistry

위의 관계로 SavedStateRegistry의 performSave/performRestore 함수에서 state를 저장하고 꺼내오는 역할을 해줍니다.(코드로 보면 onCreate에서 performRestore를 호출하고, onSavedInstanceState에서 performSave를 호출해 줍니다.)
또한 Activity/Fragment는 SavedStateRegistryOwner를 구현합니다. 이때의 구현 함수인 getSavedStateRegistry()를 통해 Activity/Fragment로부터 SavedStateRegistry를 얻어서 사용할 수 있습니다.(나중에 ViewModelFactory쪽에서 사용하게 됩니다.)

ViewModel을 생성하기 위해 SavedStateViewModelFactory를 사용합니다. 아래처럼 사용하는 건 다들 아실겁니다.

private val viewModel by viewModels { SavedStateViewModelFactory(SavedStateRegistryOwner) }

짜잔~ Factory에서 SavedStateRegistryOwner를 파라메터로 받네요. Factory는 SavedStateRegistryOwner로부터 SavedStateRegistry를 얻어오고 이것으로 부터 자신이 저장했던 state를 가져와서 ViewModel에 다시 파라메터로 넘겨줍니다.

간단하게 코드를 살펴보려면 AbstractSavedStateVMFactory의 create 함수를 보면 됩니다.
SavedStateRegistry의 consumeRestoredStateForKey를 호출해서 state를 얻고, 또한 ViewModel에서 저장한 state가 저장될 수 있도록하기 위해 SavedStateRegistry의 registerSavedStateProvider를 호출해서 지금 생성하는 ViewModel의 key와 SavedStateProvider를 등록해 줍니다.

앞에서 state를 저장할 때 performSave를 호출한다고 했습니다. 이때 저장된 모든 (key, provider)에서 provider를 꺼내와서 provider.savedState()를 통해 state를 저장합니다. ViewModel에서 저장하고자 하는 state는 SavedStateHandle에 저장되게 되는데요. SavedStateProvider의 savedSate()를 호출하면 SavedStateHandle에 저장된 state를 가져오게 됩니다.

ViewModel -> SavedStateHandle -> SavedStateProvider

2019년 10월 11일 금요일

ViewBinding

layout으로부터 view를 가져오는 가장 원초적인 방법은 findViewById를 사용하는 겁니다.
그런데 이건 워낙에 코드를 장황하게 만들어서 귀찮죠.

그래서 보통 간단하게 사용할 수 있는 Buffer Knife나 Kotlin Android Extensions를 사용합니다.

저도 이전 앱에서는 Buffer Knife를 사용했었고 현재 앱에서는 Kotlin Android Extensions를 사용하고 있습니다.

하지만 구글에서 ViewBinding을 새로 내놨으니 다음에는 ViewBinding을 써야겠네요.

https://proandroiddev.com/hello-viewbinding-goodbye-findviewbyid-edca92b397c

2018년 6월 10일 일요일

안드로이드 아키텍쳐 패턴을 알아보자

- Clean architecture에 대한 설명

Architecting Android...Reloaded

위 블로그 내용에 대한 Github 소스

https://github.com/android10/Android-CleanArchitecture-Kotlin

- Clean architecture boilerplate using the Model-View-Intent pattern

https://github.com/bufferapp/android-clean-architecture-mvi-boilerplate

- Redux 스타일로 구현하는 방식에 대한 설명. 총 8편의 씨리즈로 되어 있다.

Reactive apps with MVI

- Coordinator Pattern에 대해 알아본다.

In-app navigation with coordinators

위 블로그 내용에 대한 Github 소스

https://github.com/sockeqwe/CoordinatorsAndroid

- 많이 이야기되는 패턴에 대한 장단점 나열

결론으로 Redux 쓰지말고 MVVM 쓰라고 함

MVC/MVP/MVVM/CLEAN/VIPER/REDUX/MVI/PRNSAASPFRUICC

2018년 1월 10일 수요일

안드로이드 스터디

The Dex File Format

.java -> .class -> .dex

ART: Ahead-of-Time and Just-in-Time

D8, R8

Sinking Your Teeth Into Bytecode

sources + libraries -> compilers -> transforms -> d8 -> *.dex -> ART(Interperter, JIT, AOT) -> Machine code

- 리스트에 비디오 플레이 넣기

한번에 하나만 플레이 하도록 하기 위해 리스트 아이템 사이에 정보 전달이 필요하다.

https://medium.com/@v.danylo/implementing-video-playback-in-a-scrolled-list-listview-recyclerview-d04bc2148429

- RecyclerView는 어떻게 구현되어 있을까?

RecyclerView ins and outs - Google I/O 2016

http://blog.naver.com/PostList.nhn?from=postList&blogId=mail1001&categoryNo=14&currentPage=4

It's time to ditch Loaders in Android

Loader는 이제 그만.
Architecture Components를 사용하자.

2018년 1월 4일 목요일

안드로이드 스터디 - UI


Playing with Paths

- Cartesian coordinates vs polat coordinates
- Path, CornerPathEffect, DashPathEffect

https://gist.github.com/nickbutcher/b41da75b8b1fc115171af86c63796c5b#file-polygonlapsdrawable-kt

Understanding Android Adaptive Icons

Designing Adaptive Icons

Implementing Adaptive Icons

VectorDrawable Adaptive Icons

- What is WindowInsets?

Becoming a master window fitter

Spantastic text styling with Spans

SpannedString
SpannableString
SpannableStringBuilder

Appearance Affecting Spans vs Metric Affecting Spans

Character Affecting Spans vs Paragraph Affecting Spans

CharacterStyle
ParagraphStyle
UpdateAppearance
UpdateLayout



2017년 10월 23일 월요일

Conductor 소스 분석


Conductor
  • MainActivity: 최상단 activity
    • Conductor.attachRouter를 통해 Activity와 Activity 안의 container(FrameLayout으로 만든다.)와 savedInstanceState를 연결하는 Router를 만든다.
    • attachRouter: LifecycleHandler라는 Fragment를 만들어서 Activity에 연결한다. 실제 Router는 LifecycleHandler에서 만든다.
  • Router: stack을 통해 Controller의 go/back을 관리한다.
    • Router의 setRoot를 통해 최상단에 적용할 Controller를 연결한다. Controller는 RouterTransaction을 통해 Router에 연결된다.
  • Controller: inflateView를 통해 View를 연결한다.
  • ControllerChangeHandler: View의 변환시 Animation이나 Transition을 한다.

LifecycleHandler

  • Fragment를 상속한다.
  • setRetainInstance(true)를 통해 activity가 recreate될 때 fragment도 같이 recreate되지 않도록 한다.
  • activeLifecycleHandlers를 통해 activity마다 하나의 LifecycleHandler를 연결한다.
  • application에 자신을 ActivityLifecycleCallbacks으로 등록한다.
  • pendingPermissionRequests: 퍼미션 요청하는 것에 대한 관리
  • routerMap을 통해 container(ViewGroup)마다 하나의 ActivityHostedRouter를 연결한다.

Router

  • ActivityHostedRouter and ControllerHostedRouter
  • ActivityHostedRouter는 LifecycleHandler와 연결하고, ControllerHostedRouter는 Controller와 연결한다.
  • InstanceState를 써서 data를 유지한다: KEY_BACKSTACK, KEY_POPS_LAST_VIEW
  • setRoot를 통해 최상단 RouterTransaction을 연결한다.
    • BackStack에 RouterTransaction을 넣는다.
    • ControllerChangeHandler.executeChange를 통해 RouterTransaction의 Controller에 연결되어 있는 View를 addView한다. 이전것은 removeView한다.
    • add와 remove를 위해 ControllerChangeHandler를 사용한다. RouterTransaction에 연결된 ControllerChangeHandler가 없으면 애니메이션없는 SimpleSwapChangeHandler를 사용한다.
  • Backstack
    • ArrayDeque을 통해 백스택 구현
    • Iterator<RouterTransaction> 및 reverseIterator 제공
  • Backstack에 controller가 들어가면 아래의 라이프 관련 함수가 호출된다.
    • onContextAvailable
    • inflate
  • Backstack에서 controller가 빠지면 아래의 라이프 관련 함수가 호출된다.
    • detach
    • destroy

ControllerChangeHandler

  • Controller로부터 View를 얻어온다: inflate
  • performChange를 통해 View의 push/pop을 실행한다.
  • AnimatorChangeHandler
    • performChange 함수에서 addView를 하고 animation을 시작한다.
    • animation은 subclass의 getAnimator를 통해 얻어온다.
    • FadeChangeHandler: AnimatorChangeHandler를 상속하여 getAnimator와 resetFromView를 구현
      • getAnimator: AnimatorSet를 사용하여 기존의 뷰는 알파를 0으로, 새로운 뷰는 알파를 0에서 1로 변경한다.
      • resetFromView: 애니메이션이 끝나면 호출되는 함수. 기존의 뷰의 알파를 1로 바꾼다.

Controller

  • instanceId: UUID.randomUUID().toString()
  • 화면의 구성을 위해 inflate가 호출된다.
    • onCreateView를 호출하여 View를 생성한다.
    • subclass에서 inflateView와 onViewBound를 구현한다.
  • detach시 onSaveViewState를, inflate시 onRestoreViewState를 호출해준다.

RouterTransaction

  • Controller와 (pushChangeHandler, popChangeHandler)의 연결고리를 Router에 제공한다.

Lifecycle 관리

  • LifecycleHandler는 ActivityLifecycleCallbacks를 구현한다.
    • ActivityLifecycleCallbacks는 Application에 선언되어 있는 인터페이스이다.
    • 콜백이 불리면 Router의 Lifecycle 관련 함수를 호출한다.
  • Router는 Lifecycle 관련 함수를 정의하고 있다.
    • 이 함수에서는 Controller의 Lifecycle 관련 함수를 호출한다.
    • child router가 있으면 이의 Lifecycle 관련 함수를 호출한다.
  • LifecycleListener
    • Controller에 등록해서 Lifecycle 이벤트를 받는다.

AutoDispose

  • ControllerScopeProvider
    • Controller에서 라이프 사이클이 바뀌면 BehaviorSubject를 통해 이벤트가 발생하도록 한다.
    • lifecycle에서 아이덴티티를 숨긴 lifecycleSubject.hide()를 리턴한다.
    • correspondingEvents에서 dispose를 하기 위해 대응되는 매핑(CORRESPONDING_EVENTS)을 리턴한다.
    • peekLifecycle에서 BehaviorSubject의 현재 값을 리턴한다.

2017년 2월 16일 목요일

Android architecture를 통한 MVP, MVVM의 이해

android-architecture 를 통한 MVP, MVVM의 이해

MVP

  • TasksActivity
    • TasksFragment와 TasksPresenter를 생성하고 연결하는 controller의 역할을 한다.
  • TasksFragment와 TasksPresenter간의 연결은 TasksContract의 View와 Presenter에 의해 이루어진다.
  • TasksFragment
    • newInstance 함수를 통해 생성함수를 제공한다.
    • setPresenter를 통해 presenter를 입력받는다.
    • presenter의 start함수를 호출한다.
  • TasksPresenter
    • 초기화시 view의 setPresenter를 호출하여 자기자신을 View에 등록한다.

MVP with databinding

  • TasksActivity
    • TasksFragment와 TasksPresenter와 TasksViewModel을 생성한다.
    • TasksPresenter 생성시 TasksFragment를 파라메타로 넣어준다: new TasksPresenter(.., tasksFragment);
    • TasksViewModel 생성시 TasksPresenter를 파라케타로 넣어준다: new TasksViewModel(getApplicationContext(), mTasksPresenter);
    • TasksFragment에 TasksViewModel을 설정한다: tasksFragment.setViewModel(tasksViewModel);
  • TasksPresenter
    • TasksFragment가 바라보는 TasksContract.Presenter를 구현한다.
    • 생성자에서 TasksContract.View를 받는다: 여기서는 TasksFragment이다.
    • 생성자에서 받은 View에 presenter를 설정해준다: mTasksView.setPresenter(this);
    • start 함수를 구현한다.
    • xml 로부터의 이벤트를 받는다: addNewTask()
  • TasksFragment
    • TasksPresenter가 바라보는 TasksContract.View를 구현한다.
    • setPresenter를 구현하여 TasksContract.Presenter를 받는다: 여기서는 TasksPresenter이다.
    • 시작시 presenter의 start 함수를 호출한다: mPresenter.start();
    • layout file의 이름이 tasks_frag.xml이라면 TasksFragBinding으로 databinding을 한다: TasksFragBinding.inflate(inflater, container, false);
    • xml에 지정한 data의 값을 설정해준다: tasksFragBinding.setTasks(mTasksViewModel); and tasksFragBinding.setActionHandler(mPresenter);
    • TasksFragBinding에서 생성된 View는 getRoot 함수를 통해 얻어온다: tasksFragBinding.getRoot();
    • ListView를 위한 adapter에 presenter를 넘겨준다: ListView에서 처리하고자 하는 이벤트 발생시 presenter의 관련 함수를 호출해준다.
  • TasksViewModel
    • BaseObservable을 상속한다.
    • 생성자에서 TasksContract.Presenter를 받는다: 여기서는 TasksPresenter이다.
    • @Bindable을 통해 xml 파일과 연결한다.
    • 가져올 필요가 있는 값을 presenter를 통해 얻어온다.

MVVM

TasksActivity를 만든다. TasksActivity는 TasksFragment와 TasksViewModel을 생성한다. TasksFragment와 TasksViewModel는 서로 상호 참조를 해야 한다. 상호참조를 위해 TasksFragment는 setPresenter 함수를 통해 TasksViewModel를 입력받고, TasksViewModel은 생성시 TasksFragment를 입력받는다. 이때 서로를 직접적으로 입력받는 것이 아닌 인터페이스를 입력받는다.
TasksFragment의 기본 구성은 아래와 같다.
public class TasksFragment extends Fragment implements TasksNavigator {
  private TasksViewModel tasksViewModel;

  // 생성 함수
  public static TasksFragment newInstance() {
    return new TasksFragment();
  }

  // view model을 받는다.
  public void setViewModel(TasksViewModel viewModel) {
    tasksViewModel = viewModel;
  }
}
그리고 시작시 ViewModel의 start 함수를 호출하여 데이타 로딩같은 초기 작업이 이루어지도록 한다.
// 여기서는 onResume에서 start를 호출하지만 필요에 따라 onCreate에서 호출할 수도 있을 것이다.
@Override
public void onResume() {
  super.onResume();
  tasksViewModel.start();
}
databinding을 통해 View와 ViewModel을 연결한다.
// xml 파일의 이름이 tasks_frag.xml이므로 TasksFragBinding으로 연결이 된다.
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
  tasksFragBinding = TasksFragBinding.inflate(inflater, container, false);
  tasksFragBinding.setViewModel(tasksViewModel);
  View root = tasksFragBinding.getRoot();
  return root;
}
TasksViewModel의 기본 구성은 다음과 같다.
public class TasksViewModel extends BaseObservable {
  private final TasksNavigator navigator;

  public TasksViewModel(TasksNavigator navigator) {
    this.navigator = navigator;
  }

  public void start() {
    // 데이타를 로드한다.
    loadTasks(false);
  }
}
View와의 databinding을 위한 변수들을 설정한다. ObservableList, ObservableBoolean, ObservableField등의 다양한 databinding타입을 사용한다.(이 타입들의 변수들에 대한 설정은 get/set함수를 통해 이루어진다.) 또는 @Bindable을 사용해서 연결할 수도 있다.
// list view에서 보여지는 데이타들을 가지고 있는 변수
public final ObservableList<Task> items = new ObservableArrayList<>();
// 현재의 필터링 레벨을 보여주는 변수
public final ObservableField<String> currentFilteringLabel = new ObservableField<>();

// Bindable을 사용하는 경우에는 필요한 곳에서 아래의 형태로 호출을 해주어야 한다.
// notifyPropertyChanged(BR.empty); // It's a @Bindable so update manually
@Bindable
public boolean isEmpty() {
  return items.isEmpty();
}
여기까지가 기본적인 TasksFragment와 TasksViewModel의 구현이다. 이를 기반으로 실제 비지니스 로직이 들어가는 함수들을 구현하면 된다. 예를 들면, TasksNavigator에 필요한 인터페이스를 정의하여 TasksFragment가 이를 구현하게 하고 TasksViewModel은 View에 변경이 가해지거나(버튼 클릭등) Model이 바뀌는 경우 TasksNavigator의 필요한 함수를 호출하게 한다.
TasksFragment는 ListView를 포함하고 있다. 이 경우 ListView용의 ViewModel을 따로 만들어준다. 이의 구현을 살펴보자. ListView의 Adapter에 별도의 MVVM이 있다고 보면 될듯하다.
public static class TasksAdapter extends BaseAdapter {
  // Fragment의 TaskNavigator와 같은 역할을 하는 것
  private final TaskItemNavigator taskItemNavigator;
  // ListView에서의 처리에 TasksViewModel를 사용할 필요가 있는 경우도 있으므로 필요하면 이처럼 내부에 가지고 있는다.
  private final TasksViewModel tasksViewModel;

  public TasksAdapter(List<Task> tasks, TaskItemNavigator taskItemNavigator, TasksViewModel tasksViewModel) {
    this.taskItemNavigator = taskItemNavigator;
    this.tasksViewModel = tasksViewModel;
    // 데이타 로딩
    setList(tasks);
  }

  // TasksViewModel의 items에 변경이 일어나면 호출되기 위한 함수
  public void replaceData(List<Task> tasks) {
    setList(tasks);
  }

  @Override
  public View getView(int i, View view, ViewGroup viewGroup) {
    Task task = getItem(i);
    // xml 파일의 이름이 task_item.xml이다.
    TaskItemBinding binding;
    if (view == null) {
      // Inflate
      LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
      // Create the binding
      binding = TaskItemBinding.inflate(inflater, viewGroup, false);
    } else {
      // Recycling view
      binding = DataBindingUtil.getBinding(view);
    }

    // ViewModel을 생성한다.
    final TaskItemViewModel viewModel = new TaskItemViewModel(taskItemNavigator);
    // View와 ViewModel을 연결한다.
    binding.setViewModel(viewmodel);
    // To save on PropertyChangedCallbacks, wire the item's snackbar text observable to the
    // fragment's.
    viewModel.snackbarText.addOnPropertyChangedCallback(
        new Observable.OnPropertyChangedCallback() {
      @Override
      public void onPropertyChanged(Observable observable, int i) {
        tasksViewModel.snackbarText.set(viewmodel.getSnackbarText());
      }
    });
    viewModel.setTask(task);
    return binding.getRoot();
  }

  private void setList(List<Task> tasks) {
      this.tasks = tasks;
      notifyDataSetChanged();
  }
}
TasksViewModel이 task item의 리스트를 가지고 있다. 이의 변경이 일어나면 어떻게 ListView에 적용이 되는 걸까? tasks_frag.xml의 ListView를 보면 app:items="@{viewmodel.items}" property가 적용되어 있다. 여기에 추가하여 아래의 사용자 지정 바인딩을 사용한다.
public class TasksListBindings {

  // app:items의 값, 즉 TasksViewModel의 items에 변경이 일어나면 이 함수가 호출된다.
  // adapter의 내용을 items로 변경해준다.
  @SuppressWarnings("unchecked")
  @BindingAdapter("app:items")
  public static void setItems(ListView listView, List<Task> items) {
    TasksFragment.TasksAdapter adapter = (TasksFragment.TasksAdapter) listView.getAdapter();
    if (adapter != null) {
      adapter.replaceData(items);
    }
  }
}

Dagger2

  • Component와 Module을 정의한다.
  • Component
    • TasksRepositoryComponent에 dependency를 가지며 TasksPresenterModule을 사용하는 Component
@FragmentScoped
@Component(dependencies = TasksRepositoryComponent.class, modules = TasksPresenterModule.class)
public interface TasksComponent {
  void inject(TasksActivity activity);
}
  • Module
@Module
public class TasksPresenterModule {
  private final TasksContract.View mView;

  public TasksPresenterModule(TasksContract.View view) {
    mView = view;
  }

  @Provides
  TasksContract.View provideTasksContractView() {
    return mView;
  }
}
  • Component와 Module의 정의 후 아래와 같이 사용한다.
public class TasksActivity extends AppCompatActivity {
  @Inject TasksPresenter mTasksPresenter;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    ...
    DaggerTasksComponent.builder()
        .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
        .tasksPresenterModule(new TasksPresenterModule(tasksFragment)).build()
        .inject(this);
  }
}

2016년 12월 13일 화요일

SlidingUpPanelLayout 소스 분석

지금은 없어졌지만 Umano에서 만든 앱에 적용된 sliding up panel의 구현 소스 분석

https://github.com/umano/AndroidSlidingUpPanel

패널을 위로 스와이프하는 경우 메인 영역 어둡게 처리하기

  • ViewGroup의 child를 그릴때 drawChild가 호출되므로 여기에서 처리하도록 한다.
  • slideable view가 있고 main view인 경우에 main view를 어둡게 처리한다.
  • 그림을 그리는 영역을 canvas.getClipBounds로 얻어온다.
    • 이 영역의 bottom을 slideable view의 top과 비교하여 작은 값으로 바꿔준다. -> slideable view는 어둡게 처리할 필요 없다.
  • 지정된 fade color로부터 alpha를 얻어온 후 slide offset의 비율에 따라 어두운 정도를 계산한다.
  • 계산된 alpha로 다시 color값을 만든 후 이 color를 canvas.drawRect에 지정한다.

Shadow를 slideable view 위에 표시하기

  • draw를 override하여 구현한다.
    • draw는 drawChild에 의해 호출된다.
  • View 소스를 보면 drawing step에 대해 아래와 같은 설명이 있다.
    • 1 Draw the background
    • 2 If necessary, save the canvas' layers to prepare for fading
    • 3 Draw view's content
    • 4 Draw children
    • 5 If necessary, draw the fading edges and restore layers
    • 6 Draw decorations (scrollbars for instance)
  • shadow drawable을 xml로 만든다: gradient
  • draw(Canvas c)를 override 하여 여기서 shadow drawable을 지정한 위치에 그린다.
  • slideable view의 top으로부터 shadow drawable을 놓을 위치(drawable의 top과 bottom)를 파악한다.

onSaveInstanceState

  • slide state 정보를 저장하여 onRestoreInstanceState 호출 시 다시 저장할 수 있도록 한다.

class LayoutParams extends ViewGroup.MarginLayoutParams

  • weight를 지정하여 percentage로 height를 정할 수 있도록 한다.
  • ViewGroup의 generateLayoutParams이 호출될 때 이 LayoutParams를 생성하여 리턴하여 이 layout이 사용되도록 한다.
  • checkLayoutParams가 false를 리턴하는 경우 generateLayoutParams이 호출된다.
  • onLayout에서 margin이 크기 계산에 사용될 수 있도록하기 위해 사용된다.

class DragHelperCallback extends ViewDragHelper.Callback

  • tryCaptureView
    • dragging을 허용하는 slideable view를 리턴하도록 한다.
  • getViewVerticalDragRange
    • slide range를 리턴한다.
  • onViewDragStateChanged
    • drag state가 바뀌면 slide offset을 계산한 후 이 값에 따른 state를 저장한다.
  • onViewPositionChanged
    • slideable view의 위치에 따라 main view의 크기를 조절한다.
      • 보통은 현재 상태 그대로 유지.
      • collapsed 상태이고 overlay가 아닐 때 height를 match_parent로 변경한다.
  • onViewReleased
    • dragging하다가 손을 뗀 경우 호출된다.
    • slideable view를 최종으로 위치시킬 top 위치를 계산한 후 settleCapturedViewAt를 사용하여 적용한다.
  • clampViewPositionVertical
    • collapsed와 expanded 사이로 해서 top의 위치를 리턴한다. 이 값으로 slideable view의 위치를 이동한다.

mFirstLayout

  • layout을 다시 정하는 경우 사용된다.
  • true로 설정
    • anchor point를 새로 설정할 때
    • onAttachedToWindow와 onDetachedFromWindow가 호출될 때
    • onSizeChanged가 호출될 때 height가 변경되었을 때
  • false로 설정
    • onLayout이 끝난 경우
  • onLayout
    • slide offset 새로 계산
    • slideable view의 경우 새로 계산된 slide offset 값에 따라 새로 위치가 계산되어야 함

setWillNotDraw(false)

  • 직접 draw를 그린다는 것을 의미한다. onDraw가 호출되도록 한다.

onMeasure

  • child로 두개만 가지는지를 확인한다. 첫번째가 main view이고 두번째가 slideable view이다.
  • main view와 slideable view의 width와 height를 계산한다.
  • main view
    • overlay가 아닌 경우 height에서 panel의 height를 뺀다.
  • slideable view
    • slide range를 지정한다: height - panel height

onLayout

  • slide state에 따라 slide offset(0 ~ 1)을 계산한다.
  • slide offset에 따라 slideable view의 top을 계산한 후 이를 적용한다.
  • parallax가 지정된 경우 main view의 위치를 ViewCompat.setTranslationY함수를 사용해서 slide offset의 비율만큼 옮겨준다.

computeScroll

  • 스크롤시 호출되는 함수
  • 보통 특별한 작업이 필요한 것이 아니면 여기서 사용되는 것과 같이 쓰면 된다: continueSettling와 ViewCompat.postInvalidateOnAnimation의 조합

requestLayout과 invalidate의 사용

  • layout부터 다시 그려야 하는 경우는 requestLayout을 호출하고 view의 내용만 다시 그려야 하는 경우는 invalidate를 호출해준다.

터치 이벤트 처리

  • 뷰의 터치 이벤트 관련 함수에서 ViewDragHelper의 관련 함수를 호출해준다.
  • dispatchTouchEvent
    • dragging중일때는 바로 onTouchEvent를 호출한다.
  • onInterceptTouchEvent
    • mDragHelper.shouldInterceptTouchEvent(ev)를 호출해준다.
    • true를 리턴하면 자기 자신의 onTouchEvent를 호출하고 false를 리턴하면 자식의 onTouchEvent가 호출된다.
    • 터치 포인트가 slideable view 영역 안이 아니면 mDragHelper.cancel()를 호출하고 false를 리턴한다.
  • onTouchEvent
    • mDragHelper.processTouchEvent(ev)를 호출해준다.

터치 영역이 특정 View에서 이루어진 것인지 확인하기

  • 확인을 원하는 View의 getLocationOnScreen함수를 호출하여 스크린 기준 기준점의 위치를 알아낸다. -> viewLocation
  • 자기 자신의 getLocationOnScreen함수를 호출하여 스크린 기준 기준점의 위치를 알아낸다. 여기에 터치 포인트를 더하여 스크린의 어디에서 터치가 일어났는지 알아낸다. -> screenX, screenY
  • 앞에서 찾은 값의 비교로 터치 포인트가 View안에서 일어난 것인지 확인 가능

Generic interfaces 요점

 https://go.dev/blog/generic-interfaces  Generic interface를 정의할 때 최소한의 제약만을 정의하고 실제 구현체들이 자신만의 필요한 제약을 추가할 수 있도록 하는 것이 좋다. pointer receiver를...