[안드로이드]검색 필터UI 만들기(프레그먼트)

Lee Yongin·2022년 2월 28일
0

안드로이드

목록 보기
4/12
post-thumbnail

홈 UI를 만들고 있다. 검색바를 클릭하면 기준에 맞는 콘텐츠를 찾게 만들 필터를 보여줘야 한다. 밑으로 스르륵 smooth하게 애니메이션도 넣고 싶지만! 일단은 기초부터 만들어보았다.
Frament1 속의 검색바를 클릭하면 검색필터를 하위프레그먼트로 보여주려고 한다.

1.하위 프레그먼트(Fragment)호출하기

프레그먼트를 뷰그룹에 넣는 방법은 xml에 Fragment태그를 이용하거나, Host하는 쪽에서 FragmentTransaction을 이용하는 방법이 있다. 밑의 그림은 후자에 관해서 도식화했다.

검색바를 클릭할 때는 setOnClickListener를 쓰는 게 맞지만, 검색바에 포커스가 없으면 검색필터 프레그먼트를 접는 게 좋을 것 같아 setOnFocusListener로 구현하려고 했다.

binding.searchBar.setOnFocusChangeListener { view, b ->
            if (b) {
                //검색필터 프레그먼트 호출하기
                val fragmentManager = childFragmentManager
                val fragmentTransaction = fragmentManager.beginTransaction()
                val fragment = SearchfilterFragment()
                fragmentTransaction.add(R.id.frame_filter, fragment)
                fragmentTransaction.commit()
                //검색필터 펼치기
            }
            else{
                //검색필터 접기
            }

그런데 생각해보니까 프레그먼트를 접었다 폈다 할 수 있나??하는 생각이 들었다. 그래서 일단 프레그먼트 뷰가 삽입되는 FramLayout의 height값을 1dp에서 209dp로 동적으로 바꿔보기로 했다.

2.동적으로 View 크기 바꾸기

간단하게 이렇게 하면 좋겠지만, 고려할 게 2가지이다.

binding.frameFilter.layoutParams.height = h

1.view는 UI를 처리하는 메인스레드에서 처리해야 한다.

view의 크기를 바꾸려면 main Thread에 접근 가능한 main handler객체가 필요하다.
그 핸들러를 통해 작업 스레드에서 HeightControl(i) 함수로 view의 크기를 바꾸었다.
밑의 그림과 같은 구조이다!

2.view의 생명주기도 고려해야 한다.

내가 바꾸려는 view는 프레그먼트가 들어갈 FrameLayout이다.
이 view의 속성이 바뀔 때 호출해야 하는 메소드는 requestLayout() 아니면 invalidate()이다.
requestLayout()은 view의 경계가 바뀔 때, invalidate()는 view의 모양이 바뀔 때라는데...뭘 써야할지 잘 몰라서 모두 사용해봤는데 화면 상으로는 똑같이 나온다. Run에 나타나는 로그를 비교해도 차이가 없었다.

requestLayout():If in the course of processing the event, the view's bounds may need to be changed
invalidate():Similarly, if in the course of processing the event the view's appearance may need to be changed

1.requestLayout()을 사용한 경우 검색바를 클릭한 결과

D/InputMethodManager: prepareNavigationBarInfo() DecorView@900ced3[MainActivity]
D/InputMethodManager: getNavigationBarColor() -855310
V/InputMethodManager: Starting input: tba=com.example.bangu ic=com.android.internal.widget.EditableInputConnection@6cdced5 mNaviBarColor -855310 mIsGetNaviBarColorSuccess true , NavVisible : true , NavTrans : false
D/InputMethodManager: startInputInner - Id : 0
I/InputMethodManager: startInputInner - mService.startInputOrWindowGainedFocus
D/InputTransport: Input channel constructed: fd=75
D/InputTransport: Input channel destroyed: fd=77
D/InputMethodManager: SSI - flag : 0 Pid : 18089 view : com.example.bangu
D/InputMethodManager: prepareNavigationBarInfo() DecorView@900ced3[MainActivity]
D/InputMethodManager: getNavigationBarColor() -855310
D/ViewRootImpl@5a4eb27[MainActivity]: ViewPostIme pointer 0
D/ViewRootImpl@5a4eb27[MainActivity]: ViewPostIme pointer 1
D/ViewRootImpl@5a4eb27[MainActivity]: MSG_RESIZED: frame=[0,0][1080,2220] ci=[0,72][0,144] vi=[0,72][0,1071] or=1

2.invalidate()를 사용한 경우 검색바를 클릭한 결과

D/ViewRootImpl@d746999[MainActivity]: ViewPostIme pointer 0
D/ViewRootImpl@d746999[MainActivity]: ViewPostIme pointer 1
D/InputMethodManager: prepareNavigationBarInfo() 
D/InputMethodManager: getNavigationBarColor() -855310
V/InputMethodManager: Starting input: tba=com.example.bangu ic=com.android.internal.widget.EditableInputConnection@eb218d7 mNaviBarColor -855310 mIsGetNaviBarColorSuccess true , NavVisible : true , NavTrans : false
D/InputMethodManager: startInputInner - Id : 0
I/InputMethodManager: startInputInner - mService.startInputOrWindowGainedFocus
D/InputTransport: Input channel constructed: fd=76
D/InputTransport: Input channel destroyed: fd=75
D/InputMethodManager: SSI - flag : 0 Pid : 23468 view : com.example.bangu
D/InputMethodManager: prepareNavigationBarInfo() DecorView@764d9e5[MainActivity]
D/InputMethodManager: getNavigationBarColor() -855310
D/ViewRootImpl@d746999[MainActivity]: ViewPostIme pointer 0
D/ViewRootImpl@d746999[MainActivity]: ViewPostIme pointer 1
D/ViewRootImpl@d746999[MainActivity]: MSG_RESIZED: frame=[0,0][1080,2220] ci=[0,72][0,144] vi=[0,72][0,1071] or=1

자식 뷰의 크기와 상관없이 인위적으로 뷰의 height속성 값만 변화시켰기 때문에 mesure()~onLayout() 과정을 되풀이할 필요가 없다고 생각한다. 사용자가 입력란에 포커스를 변화시키는 게 여러번일 것을 생각해도 마찬가지이다.

일단 invalidate()를 써보는 것으로 일단락했다! 하지만 또 문제가 있다...검색바로 쓰려고 하는 EditText창의 포커스가 한 번 잡히고나면, 검색으로 넘어가지 않는 이상 포커스가 거의 0이 되지 않는 것이다. 프로그래밍적으로 큰 문제는 없지만, 사용감에 악영향을 줄지는 그 외 기능을 마무리 짓고 생각해봐야겠다.

override fun onStart() {
        val workThread = Thread(HeightControl(550))
        val workThread2 = Thread(HeightControl(0))
        super.onStart()
        binding.searchBar.setOnFocusChangeListener { view, b ->
            if (b) {
                //검색필터 프레그먼트 호출하기
                val fragmentManager = childFragmentManager
                val fragmentTransaction = fragmentManager.beginTransaction()
                val fragment = SearchfilterFragment()
                fragmentTransaction.add(R.id.frame_filter, fragment)
                fragmentTransaction.commit()
                //검색필터 펼치기
                workThread.start()
            }
            else{
                //검색필터 접기
                workThread2.start()
            }
        }
    }
    inner class HeightControl(i:Int):Runnable{
        val h = i
        override fun run() {
            myHandler.post {
                //필터 프레그먼트 화면 늘리기
                binding.frameFilter.layoutParams.height = h
                binding.frameFilter.invalidate()
            }
        }
    }

자료 출처 및 참고
https://developer.android.com/reference/android/view/View#drawing
https://academy.realm.io/kr/posts/android-thread-looper-handler/
https://brunch.co.kr/@mystoryg/84
https://www.charlezz.com/?p=29013

profile
f1을 좋아하는...🏆 f1처럼 빠르고 정확한 걸 좋아하는 안드로이드 개발자

0개의 댓글