발견록_05

김재현·2023년 5월 15일
0

안드로이드

목록 보기
7/12

1. 뷰페이저2에서 페이지 전환시 특정 부분이 뒤따라오는 것처럼 보이게 하기.

말로 설명하기가 어려운데 뷰페이저2에서 만들고자하는 Layout에 ImageView와 여러 텍스트들이 들어있는 ConstraintLayout이 있다고 하자. 뷰페이저2를 오른쪽이나 왼쪽으로 스와이프하면 고정된 Layout이 움직이기 때문에 ImageView와 ConstraintLayout이 같이 움직일 것이다.
이때, ConstraintLayout이 ImageView와 같이 움직이는게 아니라 따로 움직이도록 보여주려면 어떻게 해야할까? 마치 ConstraintLayout이 ImageView를 뒤따라오는 것처럼 말이다.

이 코드는 이전 발견록을 토대로 ViewPager2를 완전히 다 그렸다고 생각하고 작성한다.

처음 생각했을때는 ConstraintLayout의 translationX의 값을 어떻게 해서든지 더해주면 된다고 생각했다. 그래서 ViewPager의 여러 콜백함수중에서 onPageScrolledonPageSelected를 사용한 방법을 찾고자 했다. 하지만 내가 스크롤하고있는 현 페이지만 움직이거나(스와이프 앞,뒤는 적용되지 않았다.) 내가 원하는 방법만큼 움직이지 않았다.

그래서 찾은 방법이 setPageTransformer이다. 공식문서에서는 페이지 변환기는 표시되거나 첨부된 페이지가 스크롤될 때마다 호출됩니다. 이렇게 하면 응용프로그램이 애니메이션 속성을 사용하여 페이지 보기에 사용자 지정 변환을 적용할 수 있습니다.라고 설명하고있다.

어쨌든 중요한 점은 setPageTransformer를 사용하면 로딩된 ViewPager2의 뷰들에 대한 position값을 얻을 수 있다는 것이다. 로딩된 뷰라는 것은 스와이프해서 로딩된 뷰뿐만 아니라 offScreenPageLimit에도 영향을 받아서 position값이 만들어진다. 그럼 page와 position값이 쌍으로 묶여서 출력되는데 페이지가 2개가 보여지고 있으면 2쌍이 약간 스와이프 될때마다 출력된다.

빨간색 부분이 적어야할 내용이다. 그래서 translationX의 값만 position과 page에 따라서 맞춰주면 되는데, findViewById를 사용할때는 주석으로 만든 부분으로 하면 되고 viewBinding을 사용할 경우에는 현재 페이지(PageTransFormer의 page)에서 레이아웃 파일에 대한 바인딩 객체를 생성하고 constraintlayout의 id를 가진 뷰를 참조하면 된다. 초록색 밑줄인 ItemTopbnrbinding은 item_topbnr.xml파일로 viewpager2를 위한 layout파일이고 topbnrTextLayout은 constraintlayout의 id이름이다.

뷰페이저의 각 페이지에는 'item_topbnr.xml' layout 파일이 inflate 되어서 보여지고 있다. 그래서 ItemTopbnrbinding.bind(page) 코드를 통해 레이아웃 파일에 대한 바인딩 객체를 생성하고 이를 binding변수에 할당한다.
bind()메서드는 레이아웃 파일의 최상위 뷰를 기준으로 바인딩 객체를 생성한다. page에는 현재 페이지를 나타내는 뷰에 대한 정보가 담겨있기 때문에 바인딩 객체를 생성하고 레이아웃 파일의 constraintlayout뷰를 id를 통해 참조할 수 있다.

이런 page에 대한 정보가 setPageTransformer에선 로딩된 페이지 모두 나오기 때문에 화면상 보이는 각각의 페이지들에 대해서 translationX에 대한 변화를 줄 수 있게된다.


2. Multi ViewHolder 사용하기

RecyclerView를 만들기 위해서는 activity_main.xml에 <recyclerView>를 선언하고 item_layout.xml처럼 무슨 layout을 여러개 만들건지를 지정해주어야 한다.
그런데 layout을 하나만 말고 여러 layout을 RecyclerView에 넣고 싶을때가 있었는데 이때 사용한 방법이다.

자세히 말하자면, RecyclerView를 수평으로 그리다가 중간에 멈추면서 맨 오른쪽에 기존과는 다른 layout을 넣고싶었다. 이때 방법을 찾아보니 Multi ViewHolder를 사용하는 방법이 있었다.

일단 반복하고 싶은 layout.xml파일을 만든다.

item_following_etc.xml나는 이 ImageView를 기존에 존재하는 RecyclerView의 맨 오른쪽에 넣으면서 안의 글씨를 바꾸고 싶었다.

그리고 Adapter클래스를 만든다.
이때 클래스에서 RecyclerView.Adapter를 상속받을때부터 기존과 약간 다르다.

기존

Multi ViewHolder

Viewholder가 여러개이기 때문에 특정 ViewHolder를 지정해서 해주지 않는다.

그리고 무슨 ViewHolder를 지정할건지를 정하기 위한 방법이 필요한데 나는 상수를 지정해서 결정하였다.
위와 같이 기존의 layout은 VIEW_TYPE_FOLLOWING으로 하고, 새로만든 layout은 VIEW_TYPE_ETC로 할것이다. ETC_POSITION은 몇번째부터 리사이클러뷰를 멈추고 그릴건지를 정하는 상수이다.

onCreateViewHolder에서도 마찬가지로 상속받는것은 RecyclerView의 ViewHolder여야 한다. 그리고 viewType을 검사해서 무슨 타입을 쓰고있는지 확인하여 리턴해주는 값을 결정한다. 자동완성과는 다르게 뷰바인딩을 사용해서 넘겨주고있다.

이때, FollowingViewHolder와 EtcViewHolder를 직접 만드는 ViewHolder로 지금은 레이아웃이 2개를 만들기때문에 2개이고, 나중에 더 추가하고싶으면 더 추가하는 듯 하다.

ViewHolder들도 뷰바인딩을 사용한 방법으로 내가 수정하고싶은 부분을 onBindViewHolder에 넘겨주기 위해서 변수를 만들어준다.

getItemCount에서는 기존의 item.size와는 다르게 내가 원하는 개수만큼만 아이템을 만들면 되기 때문에 기존의 개수 + 마지막에 추가하는 layout을 위해 위와 같이 적는다.
getItemViewType은 onBindViewHolder에서 사용할 함수로 인자로 받은 position의 값에 따라서 현재 무슨 ViewHolder를 사용하고 있는지 알아내기 위한 함수이다.

이렇게 만들었으면 onBindViewHolder에서 여러 작업을 할 수 있다.

나는 위와 같이 작성해서 getItemViewType을 확인해서 holder를 as FollowingViewHolder를 사용하여 캐스팅해주고 사용하였다. 또한 타입을 확인해서 새로만은 layout의 타입이면 text를 내가 계산한 값으로 넣어준다고 만들어주었다.

이렇게만 만들어주고 MainActivity.kt에서 기존에 사용하던 adapter연결과 똑같이 해주기만 하면 자동으로 만들어서 나온다.

새로만든 layout을 기존의 recyclerVie처럼 계속 새로 만드는 방법이 아닌, 마지막에 한번만 딱 나오는 방법을 찾다보니 이런식으로 활용한 것이지, 다른 활용방법이 있을지도 모른다.

수정
위처럼 쓰면 ETC_POSITION을 동적으로 바꿨을때 item의 size보다 크면 오류가 나므로 getItemCount부분을 수정해준다.


3. 수동 새로고침 만들기

보통 앱을 보면 화면 상단에서 아래로 스크롤해서 더 위로 당기면, 무슨 빙글빙글 돌아가는 이미지가 나오면서 앱이 마치 새로고침하는 듯한 모습을 볼 수 있다. 이걸 앱에 추가해보고자 한다.

androidx에서 제공하는 SwipeRefreshLayout을 사용해 볼 것이다. 관련 자료는 다른 사이트에 많이 존재해서 별다른 점은 없을 것같다.

SwipeRefreshLayout을 사용하기 위해서는 gradle에 추가해준다.
그리고 xml파일에서 태그를 통해 스크롤되서 바꿀 부분을 감싸주면 되는것같은데
나는 화면에 있는 API를 다시 불러오고 싶기를 원했기 때문에 그냥 최상위 constraintlayout에 적용했다.그 다음엔 적어둔 id를 토대로 MainActivity.kt에서 새로고침 동작이 일어났을때 무엇을 동작할 건지 정의해주어야한다. 안적으면 그냥 아무것도 안한다.

onCreate에서 함수를 통해 뷰바인딩을 통해 SwipeRefreshLayout을 넘겨주게 했다.아님 그냥 binding.refresh.apply{...}같은 방식으로 해도 된다.

setColorSchemeColors를 통해 화살표의 색을 정할수있다. ContextCompat을 안쓰려면 그냥 로 적어도 된다.

setProgressBackgroundColorSchemeReource를 통해서 새로고침의 배경색을 지정할 수 있다.화살표색과 배경색을 정하면 위와같은 움직이는 새로고침이 자동으로 만들어진다.

setOnReFreshListener를 통해서 새로고침했을때의 동작을 정의해주면 된다. 난 지금 ViewPager2들이 Job을 통해서 자동 드래그가 되고있기 때문에 그걸 취소해주는 작업을 해야한다.
왜냐하면 새로고침은 위로 드래그하는 동작인데 fakeDrag를 통해 드래그가 겹쳐버리면 작업이 꼬이면서 오류가 나기 때문이다. 그후 혹시 모르니 job을 cancel()시켜준다.
그 후에 getApiData()라는 내가 만든 api를 불러오는 함수를 통해서 api에서 데이터를 다시 요청하고 자동스크롤 job을 다시 실행시켜준다.

binding.refresh.isRefreshing = false를 통해서 새로고침에 대한 작업이 끝났다고 알려준다.


4. TextView에 format형식 지정해서 값 동적으로 넣기

참고사이트

자동으로 넘어가는 뷰페이저에서 위치정보를 보여준다라던가, 남은 개수를 알아서 계산해야한다던가 xml의 text에 미리 적어둘 수 없는 내용이 있을때 사용하는 방법이다.

layout > values > strings.xml 위치에 내가 계산해주고 싶거나 동적으로 만들고 싶은 단어를 적어준다. 다음과 같은 stringformatter가 있으며위와 같은 형식으로 작성하면 되는데 1$, 2$등 순서를 적어준다.
그후 text를 지정할때 getString을 통해 strings.xml에 적었던 부분을 만들어주고, 주었던 1,2등 순서를 그대로 넣어주면 된다.


5. 앱을 나갔다가 들어오면 배경이 검은색으로 바뀌는 현상

왼쪽은 앱을 처음 실행했을때 화면이고 오른쪽화면은 이 Apply changes and Restart Activity를 누르거나 핸드폰에서 홈화면으로 나갔다가 다시 앱을 들어왔을때 나오는 화면이다.

원래라면 정상적으로 하얀색이 나와야하는게 맞는데 무슨 이유에서인지 배경이 검은색으로 나온다. 그리고 화면을 내릴수록 점점 하얀색으로 변하는걸 확인할 수 있다.

이런 화면을 내리면 색이 변하게 하는 코드는 actionbarW의 코드에서밖에 적지 않았었다.
이런식으로 적었었는데 이 부분에서는 전체 배경색을 바꾸겠다는 말도 없고 그냥 한 부분의 constraintlayout만 색을 바꾼다. 전체 배경색을 바꾸는게 아니다.

이 현상이 일어나는 이유는 setBackgroundResource를 썼기 때문이었다.

찾아보니 setBackgroundResource는 Drawable object를 넣는것을 원칙으로 하는것 같았다. 그래서 R.color는 조건에 맞지 않아서 버그가 나는 것으로 보인다. 대신 setBackgroundColor를 지정하여 color의 id를 ContextCompat.getColor를 통해 지정해주는 방식으로 하면 배경색이 이제 오류가 나지 않는다.
코드 수 조금 줄이겠다고 setBackgroundColor에서 setBackgroundResource로 바꿨더니 이 사단이 났다.


6. BindingAdapter사용해서 view의 속성을 Custom으로 추가시키기(with. LiveData)

API를 통해 데이터를 가져오고 RecyclerView나 ViewPager2에 매개변수로 값을 넣어준다음에 Layout에 넣을 값들을 하나씩 설정해주었다. 이 방식을 DataBinding을 이용하여 자동으로 넣어주는 방법으로 바꾸어보자.

원래라면 ViewHolder와 onBindViewHolder를 이용하여 원하는 부분에 값을 일일이 넣어주어야 했다.

위 코드를
이렇게 줄이고 BindingAdapter파일을 하나만듬으로써 사용할 수 있다.

build.gradle에 종속성을 추가하고 시작한다.

kotlin-kapt 추가하기

처음으론 값을 자동으로 넣을 파일을 DataBinding을 사용하기 위한 형식으로 변경한다.이때 name은 변수명이고 type은 어떤형식의 데이터가 들어가는지를 정해주는 곳의 파일을 적으면된다.

안드로이드 공식문서 - DataBinding 레이아웃을 참고하여 각 View들에 들어갈 것을 적어준다.

예를 들어 위와 같이 적으면 되는데 나는 API에서 데이터를 가져오기 위해서 data파일에 따로 변수명을 다 적었기 때문에 type도 그렇고 가져올때 이름도 위와 같이 적었다. topBnrImage는 BindingAdapter에서 사용할 이름이다. 이런 변수명은 android:text와 같이 값이 들어오면 자동으로 들어가는 것 이외에 추가로 Glide와 같이 작업이 필요한 경우에 사용할 수 있다.

다음으론 BindingAdapters파일을 만든다.
안드로이드 공식문서 - BindingAdapter를 참고하였다.

BindingAdapter파일을 분리하였기 때문에 object를 사용하여 singleton패턴으로 만들어야한다. 그래야 setTopBnrImage같은 함수를 dataBinding에서 바로 사용할 수 있다.(우리가 직접 저 함수를 쓰지는 않는다.)
@BindingAdapter어노테이션에 xml에서 적었던 변수명을 적어서 어디에 넣을 것인지 지정해준다.
@JvmStatic을 사용하지 않으면 오류가 난다. 파일을 나눴기 때문에 setTopBnrImage가 static이라는 것을 명시해주자.

이제 RecyclerView나 ViewPager2의 어뎁터 부분에서 같이 만들어주면 된다. CustomViewHolder의 함수의 매개변수로는 Adapter를 만들때 넘겨줄 리스트파일을 넘겨준다고 생각하고 databinding한 xml파일에 넣어준다. 그 후 onBindViewHolder에서는 만들었던 함수를 통해서 실제로 값을 넣어주면 자동으로 데이터가 들어간다. 이때 android:text처럼 값을 넣으면 바로 들어가는 부분은 값을 넘겨준 리스트의 변수명과 같은 부분을 따라가서 알아서 갱신되고, BindingAdapter이름으로 만든 부분은 우리가 만들었던 BindingAdapter파일의 함수에 따라서 동작할 것이다.

추가
나는 MainActivity에서 setPageTransform을 통해서 ViewPager2의 layout파일을 한번 bind해서 객체를 만들었다. 그런데 layout파일을 databinding을 해버리면 그 객체를 인식할 수 없는 문제가 생기나 보다.

일단 Adapter에 연결한 후에 setPageTransform을 실행하므로 처럼 안에서 binding.root.tag를 이용해 뷰에 바인딩 객체를 설정하고 밖에서 자유롭게 binding.getTag 또는 binding.tag를 사용하여 객체를 가져올수 있게 한다.그 후 MainActivitiy에서 만들어놓은 tag를 사용하여 동작하도록 한다.


7. SwipeRefreshLayout을 통해서 api를 새로 불러올때, itemDecoration에서 margin이 계속 추가되는 문제

새로고침 전새로고침 3번 후

위 사진과같이 원래는 margin이 다음과 같이 정상적으로 만들어져있는데 SwipeRefreshLayout을 통해 api를 새로고침할때마다 margin이 추가되는 현상이 일어났다.

ViewModel을 통해서 api를 불러오고 api를 불러오면 LiveData를 통해서 관측하고있다가 LiveData에 변경이 일어나면 MainActivity의 observe에서 데이터와 RecyclerView의 Adapter와 연결하는 등의 작업을 한다. 여기서 RecyclerView의 addItemDecoration을 통해서 각 Recycle Item마다 다른 마진을 주기 위해서
이런 파일을 만들었다. log을 찍어보면 Main의 ItemDecoration은 한번만 실행되는데 내부에서 새로고침을 한 횟수만큼 반복해서 실행하는 모습을 볼 수 있다.한번 새로고침하면 1번, 두번째 새로고침하면 2번, 세번째 새로고침하면 3번,, 이런식으로 반복해서 실행되는 문제를 볼 수 있었다.

분명 outRect는 값을 줄때마다 이전의 마진을 전부 초기화하고 새로 마진을 완성시킨다고 하는데 대체 왜 값이 중복해서 실행되는것일까?

이유는 ItemDecoration이 중복으로 추가되기 때문이었다..
코드를 보면 api로 observe할때마다 실행되는것을 볼 수 있다. 로그를 찍어보면 한번만 ItemDecoration이 실행되는 것은 맞지만 ItemDecoration은 내가 없애주지 않는한 계속 덧붙여서 추가된다. 즉, 새로고침을 할때마다 itemDecoration이 쌓이고 있었다는 이야기이다.

해결측은 크게 두가지 인것 같다.
1. ItemDecoration을 adapter외부에서 생성하고 RecyclerView를 초기화할 때 한번만 추가되도록 하기. 즉, observe안에 쓰지 않고 onCreate()단의 새로고침하기 전에 적어주는 방법
2. ItemDecoration을 제거한 후, 새로운 데이터에 맞게 ItemDecoration을 다시 추가해주기.
removeItemDecoration을 통해서 데코레이션을 제거하고 하는 방법이다.


profile
배운거 정리하기

0개의 댓글