본문 바로가기

Android

[펌] 개선된 Pull To Refresh 라이브러리, Android Pull To Refresh 라이브러리

네이버 개발자 블로그인 헬로우 월드에 기재된 내용입니다.

출처 사이트에 가셔서 보시는걸 권해드립니다.*^^*

출처 : http://helloworld.naver.com/helloworld/textyle/675437

 

개선된 Pull To Refresh 라이브러리, Android Pull To Refresh 라이브러리 


 

네이버 비즈니스 플랫폼 웹플랫폼개발랩 김원준, 정성학

네이버 비즈니스 플랫폼은 Chris Banes의 Pull To Refresh 라이브러리를 개선한 Android Pull To Refresh 3.1.0을 최근 공개했습니다. 개선한 라이브러리에서는 레이아웃을 쉽게 변경할 수 있고 Gmail 앱에서 볼 수 있는 Google 스타일의 Pull To Refresh UI를 사용할 수 있습니다. 또한 탄성 계수, 스크롤 애니메이션 시간 등도 설정할 수 있습니다.

Chris Banes의 Pull To Refresh 라이브러리는 2013년 2월부터 관리되지 않고 있었는데 네이버 비즈니스 플랫폼이 이를 살려 내어 이슈를 해결하고 기능을 추가하고 있습니다. 이 글에서는 Chris Banes의 Pull To Refresh 라이브러리를 Android Pull To Refresh 3.1.0으로 개선한 이유를 살펴보고, 어떤 부분을 개선했고 개선 과정에서 무엇을 고려해야 했는지 설명하겠습니다.

Android Pull To Refresh 라이브러리 개요

'Pull To Refresh(당겨서 새로 고침)'라는 UI가 세상에 나온 지도 5년이 되었다. 그동안 사용자의 손은 Pull To Refresh UI에 익숙해졌고 많은 앱이 Pull To Refresh를 기본 UI로 사용하게 되었다.

iOS 6은 UIRefreshControl 객체로 이 UI를 제공하여 개발자들이 쉽게 Pull To Refresh UI를 구현하게 했다. 하지만 Android SDK는 아직 Pull To Refresh UI를 기본 UI 컴포넌트로 제공하지 않는다.

대신 Android 앱 개발자는 오픈소스로 공개된 Pull To Refresh 라이브러리를 이용한다. 대표적인 라이브러리로 두 가지가 있는데, 하나는 Johan Nilsson의 Pull To Refresh 라이브러리이고 다른 하나는 이를 계승한 Chris Banes의 Pull To Refresh 라이브러리이다. 둘 중에서는 Chris Banes의 Pull To Refresh 라이브러리가 훨씬 잘 알려져 있고 많이 쓰이고 있다.

이 글에서는 네이버 비즈니스 플랫폼이 Chris Banes의 Pull To Refresh 라이브러리(이하 Pull To Refresh 라이브러리)를 개선해서 Android Pull To Refresh 3.1.0(이하 Android Pull To Refresh 라이브러리)로 공개한 내용을 소개하려 한다. 또한 앞으로도 계속해서 개선해야 할 필요성도 설명하고자 한다.

Pull To Refresh 라이브러리 개선 이유

왜 Pull To Refresh 라이브러리를 개선해야 했는지 먼저 알아보자. 개선 이유는 두 가지로 나누어 생각할 수 있다.

사용이 중단된 프로젝트

앞에서 언급했듯 Pull To Refresh UI를 Android SDK가 기본으로 제공하지 않기 때문에 Pull To Refresh 라이브러리는 이 빈틈을 채우는 중요한 역할을 하고 있다. 그러나 2013년 2월부터 이 라이브러리는 사용이 중단된(deprecated) 상태가 되어 더 이상 관리되지 않는다.

라이브러리를 개선하지 않으면 호환성 문제에 직면할 수 있다. SDK가 업데이트되면서 하위 호환성을 지키지 않는 경우가 있어 전에는 잘 동작하던 라이브러리도 SDK의 상위 버전에서는 문제를 일으킬 가능성이 크다. 이 잠재된 위험을 해소하기 위해서는 라이브러리를 중앙에서 관리하고 지원하는 역할이 있어야 한다. 필자는 그 역할을 직접 맡기로 했다.

참고
SDK가 업데이트되면서 하위 호환성을 지키지 않아 상위 버전 SKD에서 라이브러리가 제대로 동작하지 않는 사례가 있다. Universal Image Loader 라이브러리의 사례이다. Android KitKat에서 BitmapFactory 코드가 수정되어 라이브러리에 오류가 발생했다. 더 자세한 내용은 Universal Image Loader 라이브러리의 이슈 문서를 참고한다.

그전에 먼저 Pull To Refresh 라이브러리를 분석할 필요가 있었다. 라이브러리를 접하고 분석하며 발견한 문제점이 개선의 두 번째 이유이다.

불편한 레이아웃 변경

Pull To Refresh도 화면에 보이는 UI이므로 앱의 일관적인 레이아웃에 맞게 아이콘이나 레이블 등을 변경(customization)해야 한다. 그런데 라이브러리의 확장성에 한계가 있어 그동안에는 라이브러리의 소스 코드를 직접 수정하는 방식으로 변경했다.

그래서 라이브러리를 공통적으로 사용하기 힘들고, 버전을 관리하기 어려워 이슈에 대응하기 어렵다는 문제를 안고 갈 수밖에 없었다. 레이아웃 변경에 대한 문제는 "Pull To Refresh 라이브러리의 문제"에서 자세히 설명하겠다.

Pull To Refresh 라이브러리의 구성 요소와 동작

무엇이 개선되었는지 살펴보기 전에 Pull To Refresh 라이브러리의 동작과 구조를 먼저 살펴보자.

Pull To Refresh 라이브러리의 구성 요소

Pull To Refresh 라이브러리는 크게 네 가지 구성 요소로 이루어져 있다.

  • Pull To Refresh Base
  • 로딩 레이아웃(loading layout)
  • 몸체(refreshable view)
  • 인디케이터 레이아웃(indicator layout)

6a6d936e5f62bda31690a004096b4be8.png

그림 1 Pull to Refresh UI의 구성요소

각각 무엇인지 알아보자.

Pull To Refresh Base

Pull To Refresh Base는 Pull To Refresh의 실제 동작을 담당하는 객체다. 로딩 레이아웃과 인디케이터 레이아웃, 몸체를 자식 UI로 갖고 있다. 동작에 맞게 그 안의 레이아웃을 지휘하고 감독하는 역할을 한다.

로딩 레이아웃(Loading Layout)

로딩 레이아웃은 레이아웃의 상단(header)이나 하단(footer) 아니면 상하단에 위치해 있다. 평소에는 화면 밖에 숨어 있다가 위로(아래로) 잡아당기면 로딩 레이아웃이 보이면서 잡아당겼을 때와 새로 고칠 때 안내자 역할을 한다.

그림 2 로딩 레이아웃이 보이는 모습

Pull To Refresh 라이브러리는 세로(vertical) 방향의 뷰도 지원하지만 가로(horizontal) 방향 뷰도 지원한다. 만약 가로 방향 뷰라면 상단(header)과 하단(footer)은 각각 왼쪽과 오른쪽에 위치한다.

로딩 레이아웃은 아이콘과 레이블로 구성된다. 아이콘은 당길 때 아이콘과 새로 고칠 때 아이콘이 있다. 레이블에는 당길 때 레이블과 당김이 끝날 때 레이블, 새로 고칠 때 레이블이 있다.

a69c3f559f24e2e64f9e6781a2e28b62.png

그림 3 로딩 레이아웃의 아이콘과 레이블

몸체(Refreshable View)

몸체는 실제 내용이 들어가는 자리이다. 원래 이름이 몸체는 아니지만 곤충을 머리, 가슴, 배로 구분하는 것과 같이 알기 쉬운 구분 명칭이 필요하여 몸체라 표현했다.

ListView나 WebView, ScrollView, GridView 등과 같은 객체가 바로 몸체이다. Pull To Refresh 라이브러리를 실제로 사용할 때에는 PullToRefreshListView나 PullToRefreshWebView, PulltoRefreshScrollView, PullToRefreshGridView 같은 객체를 사용한다. 'Pull To Refresh 기능이 들어간 ListView'를 사용하고 싶다면 PullToRefreshListView 객체를 사용하는 방식이다. 각 객체는 실제로는 PullToRefreshBase 클래스를 오버라이드한 클래스이고, 이 클래스들은 해당 UI를 추가한 구현체라 할 수 있다.

dabf38c5029c85744ef605a2b2028b59.png

그림 4 Pull To Refresh UI의 상속 구조

인디케이터 레이아웃(Indicator Layout)

인디케이터 레이아웃은 다음 그림에서 보듯이 인디케이터 레이아웃은 스크롤했을 때 잡아당길 수 있는 위치에 있는지 알려주는 안내자 역할을 한다. 인디케이터 레이아웃은 몸체가 AdapterView 클래스의 자손 클래스일 경우에만 사용할 수 있는 레이아웃이다. AdapterView 클래스의 대표적인 자손 클래스는 ListView 클래스다. 거의 쓰이지 않는 레이아웃이다.

그림 5 인디케이터 레이아웃이 보이는 화면

Pull To Refresh UI의 동작 순서

잡아당겼을 때(pulling)부터 새로 고침(refresh)이 끝날 때까지 다음과 같은 진행 과정을 거친다.

e08d9daeebcb39b7eb12f6628027e1df.png

그림 6 Pull To Refresh UI의 동작 순서

Pull To Refresh UI를 경험한 사람이라면 개발자가 아니더라도 이해할 수 있는 지극히 상식적인 과정이다. 다만 개발자라면, 그리고 이 라이브러리를 사용하는 개발자라면 주의해서 봐야 할 부분이 있다. 새로 고침 상태가 자동으로 완료되는 상태로 바뀌지 않고 사용하는 쪽이 직접 알려줘야 한다.

새로 고침 상태의 완료를 알려주지 않아 문제가 발생한 실제 사례를 살펴보자. 이 라이브러리를 사용하는 어떤 앱에는 새로 고침 상태가 되면 네트워크에서 새로운 정보를 가져오는 코드가 있다. 정보를 가져온 후 Pull To Refresh Base에 새로 고침 완료를 알리는 부분도 잊지 않고 구현했다. 하지만 네트워크 오류가 발생하여 Exception이 발생했을 때 새로 고침이 끝났다는 것을 알려주지 못 했다. 그래서 UI는 무한히 새로 고침 상태로 남게 되는 상황이 종종 일어났다.

이렇게 네트워크 오류처럼 생각지 못한 상황이 발생할 수 있으므로 모든 상황을 고려하여 새로 고침이 끝났다는 것을 Pull To Refresh Base에게 알려주어야 한다. 그래서 개선한 Android Pull To Refresh 라이브러리에 라이브러리 자체적으로 새로 고침 타임아웃을 설정하는 기능을 넣을지 고민 중이다.

Pull To Refresh 라이브러리의 문제

이제 본격적으로 Pull To Refresh 라이브러리가 가지고 있던 문제를 알아보자.

라이브러리라면 패키지 파일을 불러와 객체와 함수를 호출하는 방식으로 사용하고, 현재 버전에 문제가 있을 때 문제가 해결된 좀 더 높은 버전으로 대체하여 사용할 수 있어야 한다. 하지만 Pull To Refresh 라이브러리는 라이브러리로서의 자격을 충족하지 못한다.

로딩 레이아웃과 인디케이터 레이아웃 변경 문제

앞에서 설명했듯이 앱에서 Pull To Refresh 라이브러리를 사용할 때는 앱의 일관된 레이아웃을 위해서 레이아웃을 변경해야 한다. 레이아웃을 변경하려면 어떤 부분을 수정해야 할까?

어차피 몸체는 getRefreshableView() 메서드로 가져와 변경하면 되니 문제가 없다. Pull To Refresh Base는 다른 레이아웃을 포함하고 있을 뿐 자신이 실제로 화면에 표현되는 부분은 없다. 따라서 남은 건 로딩 레이아웃과 인디케이터 레이아웃뿐이다.

먼저 로딩 레이아웃을 살펴보자. LoadingLayout 클래스를 오버라이드한 두 가지 구현체는 라이브러리 내에 존재한다. 하나는 RotateLoadingLayout 구현체와 FlipLoadingLayout 구현체이다.

4429b86732de02977fa71854b3791813.png

그림 7 RotateLoadingLayout 구현체(왼쪽)와 FlipLoadingLayout 구현체(오른쪽)

이 로딩 레이아웃을 수정하여 원하는 생김새의 로딩 레이아웃을 만드는 상황을 가정해 보자. 기존에는 없던 새로운 레이아웃을 상상하고 이를 구현하여 기존의 두 개의 레이아웃 대신 사용하려 한다.

가장 자연스럽고 깔끔해 보이는 방법은 기존의 로딩 레이아웃과 상관없는 새로운 로딩 레이아웃을 구현한 후, 라이브러리 내부를 전혀 건드리는 것 없이 이 새로운 로딩 레이아웃을 사용하게 하는 것이다.

새 로딩 레이아웃의 클래스 이름을 NewLoadingLayout이라 하자. 로딩 레이아웃을 바꾸려면 다음과 같이 레이아웃 정의 XML 파일에 ptrAnimationStyle 속성을 추가하면 된다.

예제 1 새 로딩 레이아웃 정보를 추가한 레이아웃 정의 XML 파일

 

<com.handmark.pulltorefresh.library.PullToRefreshListView
xmlns:ptr="http://schemas.android.com/apk/res-auto"
...
ptr:ptrAnimationStyle="new"
/>

 

이 속성에 어떤 값이 들어갈 수 있을까? 라이브러리 내의 res/values/attrs.xml 파일에서 확인할 수 있다.

예제 2 res/values/attrs.xml 파일의 기본 정보

 

<!-- Style of Animation should be used displayed when pulling. --> 
<attr name="ptrAnimationStyle">
<flag name="rotate" value="0x0" />
<flag name="flip" value="0x1" />
</attr>

 

ptrAnimationStyle 속성에 자신이 직접 만든 로딩 레이아웃 항목을 추가해야 한다. 원하는 대로 라이브러리 바깥에서 속성의 항목을 추가하는 것은 불가능하다. Android는 attrs.xml 파일의 내용을 외부에서 확장하는 기능이 없기 때문이다. 따라서 다음과 같이 라이브러리 내의 attrs.xml 파일을 직접 수정하는 수밖에 없다.

 

<!-- Style of Animation should be used displayed when pulling. --> 
<attr name="ptrAnimationStyle">
<flag name="rotate" value="0x0" />
<flag name="flip" value="0x1" />
<flag name="new" value="0x1" /> <!-- 항목 추가 -->
</attr>

 

하지만 여기서 끝이 아니다. 다음과 같이 PullToRefreshBase 클래스의 소스 코드로 들어가 클래스의 AnimationStyle 내용을 직접 바꿔야 한다.

예제 3 com/handmark/pulltorefresh/library/PullToRefreshBase.java

 

public abstract class PullToRefreshBase<T extends View> extends LinearLayout implements IPullToRefresh<T> { 
...
public static enum AnimationStyle {
ROTATE,
FLIP,
NEW;
...
LoadingLayout createLoadingLayout(Context context, Mode mode, Orientation scrollDirection, TypedArray attrs) {
switch (this) {
case ROTATE:
default:
return new RotateLoadingLayout(context, mode, scrollDirection, attrs);
case FLIP:
return new FlipLoadingLayout(context, mode, scrollDirection, attrs);
case NEW:
return new NewLoadingLayout(context, mode, scrollDirection, attrs);
}
}
...
}
}

 

위 코드에서 빨간색이 추가한 내용이다. 속성과 로딩 레이아웃의 매핑은 이와 같은 방법으로 이루어진다. 플래그의 Integer 값과 Enum 값을 연결해 createLoadingLayout() 메서드에서 Enum 값에 맞는 클래스의 새 인스턴스를 받아온다. NewLoadingLayout 클래스 파일은 라이브러리 내에 넣어야 한다.

이렇게 라이브러리 내의 소스 코드를 수정하고 나서야 추가한 레이아웃을 사용할 수 있다. 레이아웃 변경은 완료되었지만 무엇인가 객체지향의 원리에 위배된 것 같은 이상한 느낌이 들지 않는가?

만약 좀 더 간단하게 로딩 레이아웃에 있는 레이블(화면에 나오는 메시지)만 바꾸고 싶다면 어떻게 할까? "당겨서 새로 고침"을 "더 당기세요"라고 바꾸고 싶다면? 레이블을 변경할 수 있는 속성도 있지 않을까 싶지만 애석하게도 그런 속성은 존재하지 않는다. 이 경우도 역시 라이브러리 내부에서 메시지가 저장되어 있는 리소스를 수정하거나 로딩 레이아웃 소스 코드를 직접 수정해야 한다.

이렇게 레이아웃을 변경하면서 사용자들은 자신의 필요에 맞게 Pull To Refresh 라이브러리를 직접 수정해 왔다. 라이브러리를 각자 수정하면 버전을 관리하기 어려워지는 것은 당연하다. 만약 Pull To Refresh 라이브러리의 여러 버그가 고쳐지고 기능이 추가되어 배포되었다면 과연 사용자들은 지금 사용하고 있는 라이브러리를 업데이트할까?

이는 객체지향 설계의 원칙 중 OCP(open-closed principle) 원칙을 위반할 때 발생하는 문제의 좋은 사례가 아닐까 싶다. 이 문제의 원인이 Pull To Refresh 라이브러리만의 문제라 생각하지 않는다. 근본엔 Android 라이브러리 구조상의 한계가 있다. 특히 리소스나 속성 같은 것을 라이브러리 외부에서 확장할 수 없는 부분이 문제를 유발했다고 본다.

인디케이터 레이아웃은 어떤가? 인디케이터 레이아웃은 아예 처음부터 변경이 불가능하게 설계되어 있다. 고정된 레이아웃만 볼 수 있고, 무엇인가 바꾸려면 역시 라이브러리 내부의 코드를 수정해야 한다.

Android 라이브러리의 패키지화

이렇게 Pull To Refresh 라이브러리는 변경에 취약하고 확장이 어려운 문제가 있다. 그리고 이 문제는 사실 Android 라이브러리의 구조적 한계가 근본이라고 언급했다. Android 라이브러리는 또 다른 문제가 있다. 바로 패키지 문제이다.

Android 라이브러리는 기존 라이브러리와 다르게 리소스를 포함할 수 있다. 하지만 리소스를 포함하는 라이브러리는 jar 파일같이 패키지화될 수 없어서 라이브러리도 프로젝트로 관리해야 하는 수고가 따른다.

Maven에서는 이를 해결하기 위해 apklib 포맷을 만들어 jar 파일처럼 패키지 형태로 사용할 수 있게 했다. 그리고 Gradle에서도 aar이라는 포맷을 만들어 비슷한 역할을 한다. 그러나 apklib이 패키지화를 완벽히 해주는 것은 아니다. IDE가 Eclipse라면 Maven을 사용하지 않을 때와 마찬가지로 라이브러리를 프로젝트로 관리해야 한다. 이 문제는 아마 Eclipse 플러그인이 아직 Apklib의 리소스에 완전히 대응하지 못하기 때문일 것이다. 추후에는 해결될 부분이라 예상한다.

문제점을 개선한 Android Pull To Refresh 라이브러리

개선 목표

Pull To Refresh 라이브러리를 개선하기 전에 다음과 같은 개선 목표를 설정했다.

  • OCP(open-closed principle) 원칙을 깨지 않고 로딩 레이아웃과 인디케이터 레이아웃을 변경할 수 있게 한다. Android 라이브러리는 확장할 수 있는 요소를 제공하기 어렵다는 한계가 있으므로 어떻게 확장성을 제공할 수 있을지 고민해야 한다.
  • 하위 호환성을 제공한다. 다시 말해 기존 라이브러리 사용자도 별다른 변경 사항 없이 개선 버전을 사용할 수 있도록 한다. 적어도 불가피하게 변경해야 하는 부분을 최소화한다. 그래서 사용자로 하여금 deprecated 상태의 프로젝트를 벗어나 개선 버전을 사용하도록 유도한다.

첫 번째 목표에서 확장할 수 있는 요소를 가능한 대로 제공하도록 한다고 했다. 하지만 그러면 호환성을 포기해야 하는 지점이 존재한다. 두 목표가 서로 모순된다. 그래서 일단은 하위 호환성을 지원하는 것으로 시작해서 차근차근 개선의 범위를 넓히는 것으로 방향을 정했다. 그 외 목표들도 다음과 같이 설정했다.

  • 문제점 해결 이외에도 제공하면 도움이 될만한 기능을 추가한다.
  • 오픈소스 프로젝트로 외부에 공개한다(이 부분은 "오픈소스 공개"에서 자세하게 이야기하겠다).

자유로운 로딩 레이아웃 추가

더 이상 라이브러리 내부의 무언가를 수정할 필요 없이 라이브러리 바깥의 프로젝트에서 로딩 레이아웃을 추가할 수 있게 수정했다.

먼저, 라이브러리의 res/values/attrs.xml 파일의 로딩 레이아웃 매핑 정보를 정리했다.

예제 4 라이브러리 내 res/values/attrs.xml 파일

 

<!-- Style of Animation should be used displayed when pulling. --> 
<attr name="ptrAnimationStyle">
<flag name="rotate" value="0x0" />
<flag name="flip" value="0x1" />
</attr>

 

위와 같은 부분을 다음과 같이 변경했다.

 

<!-- Style of Animation should be used displayed when pulling. --> 
<attr name="ptrAnimationStyle" format="string">
</attr>

 

속성의 데이터 타입을 Enum에서 String으로 바꿈으로써, 속성 사용 시 플래그에 존재하지 않는 속성 값을 입력하면 오류를 보여주는 편의성은 포기해야 했다. 하지만 호환성은 유지할 수 있다. 따라서 기존의 Pull To Refresh 라이브러리 사용자가 Android Pull To Refresh 라이브러리로 옮기더라도 기존에 사용했던 속성을 수정할 필요가 없다. 그리고 로딩 레이아웃을 무한히 추가할 수 있는 확장성을 얻었다.

그렇다면 속성 값으로 입력한 String을 어떻게 로딩 레이아웃 객체와 매핑할까? 이것은 따로 XML 파일을 정의하여 해결했다. 라이브러리 내의 res/xml 디렉터리에 다음과 같이 pulltorefresh.xml이라는 XML 파일을 만들었다.

예제 5 라이브러리 내 res/xml/pulltorefresh.xml 파일

 

<?xml version="1.0" encoding="UTF-8"?> 
<PullToRefresh>
<LoadingLayouts>
<layout name="rotate">com.handmark.pulltorefresh.library.internal.RotateLoadingLayout</layout>
<layout name="flip">com.handmark.pulltorefresh.library.internal.FlipLoadingLayout</layout>
</LoadingLayouts>
...
</PullToRefresh>

 

라이브러리는 이 XML 파일을 읽어서 name 속성 값에 입력한 ptrAnimationStyle 속성 값과 일치하는 layout 요소를 찾는다. layout 요소의 내용은 해당 로딩 레이아웃의 클래스 경로이므로 reflection으로 클래스 토큰을 가져와 로딩 레이아웃의 인스턴스를 생성한다.

라이브러리를 외부 참조하는 프로젝트에서 로딩 레이아웃을 새로 구현하고 이를 사용하고 싶다면 어떻게 할까? 이 부분은 프로젝트에 존재하는 assets/pulltorefresh.xml 파일을 정의하면 된다.

예제 6 assets/pulltorefresh.xml 파일

 

<?xml version="1.0" encoding="UTF-8"?> 
<PullToRefresh>
<LoadingLayouts>
<layout name="new">com.myproject.layout.NewLoadingLayout</layout>
</LoadingLayouts>
...
</PullToRefresh>

 

그런데 왜 하필 assets 디렉터리인지 궁금할 수도 있다. 이유는 런타임에 라이브러리가 외부의 프로젝트에 있는 정보를 가져오기 위해 접근할 수 있는 디렉터리가 assets 디렉터리 외에는 없기 때문이다.

라이브러리는 res/xml/pulltorefresh.xml 파일에 있는 기본 정보와 사용자가 정의한 assets/pulltorefresh.xml 파일에 있는 정보를 더하여 정보를 저장한다. 그렇다고 assets/pulltorefresh.xml 파일을 반드시 정의해야 하는 것은 아니다. 기본 로딩 레이아웃만 사용한다면 따로 정의할 필요가 없다.

변경 가능한 인디케이터 레이아웃

인디케이터 레이아웃도 로딩 레이아웃과 대칭적인 구조를 갖도록 수정하고 레이아웃을 무한히 추가할 수 있게 했다.

예제 7 assets/pulltorefresh.xml 파일

 

<?xml version="1.0" encoding="UTF-8"?> 
<PullToRefresh>
...
<IndicatorLayouts>
<layout name="custom">com.myproject.layout.CustomIndicatorLayout</layout>
</IndicatorLayouts>
</PullToRefresh>

 

추가한 인디케이터 레이아웃을 사용하려면 다음과 같이 XML 파일을 수정한다.

예제 8 레이아웃 정의 XML 파일

 

<com.handmark.pulltorefresh.library.PullToRefreshListView 
xmlns:ptr="http://schemas.android.com/apk/res-auto"
...
ptr:ptrIndicatorStyle ="custom"
/>

 

새로운 인디케이터 레이아웃을 만들고 싶을 때에는 IndicatorLayout 클래스를 오버라이드하면 된다.

 

pubic class CustomIndicatorLayout extends IndicatorLayout { 
...
}

 

IndicatorLayout 클래스에는 아무 레이아웃도 정의되어 있지 않기 때문에 오버라이드한 클래스에서 직접 정의해야 한다. 만약 기존 인디케이터 레이아웃을 그대로 사용하고 싶다면 DefaultIndicatorLayout 클래스를 오버라이드하여 구현하면 된다.

탄성 계수 및 스크롤 시간 간격 설정

탄성 계수는 잡아당길 때의 탄성을 말한다. 탄성 계수가 높을수록 잡아당기기 힘들어진다. 스크롤 시간 간격은 몸체(refreshable view)의 체감 스크롤 속도를 나타낸다. 값이 높을수록 천천히 스크롤된다.

원래 이 두 속성은 상수였지만 Android Pull To Refresh 라이브러리에서는 사용자가 직접 설정할 수 있게 했다. 속성 값을 지정하여 설정할 수도 있고 메서드를 호출하여 설정할 수도 있다.

예제 9 레이아웃 정의 XML 파일에서 속성 값 설정

 

<com.handmark.pulltorefresh.library.PullToRefreshListView 
xmlns:ptr="http://schemas.android.com/apk/res-auto"
...
ptr:ptrFriction="3.0"
ptr:ptrSmoothScrollDuration="400"
/>

 

예제 10 메서드를 호출하여 속성 값 설정

 

pullToRefreshListView.setFriction(4.0); 
pullToRefreshListView.setSmoothScroll(400);

 

레이블과 아이콘 수정

로딩 레이아웃의 레이블도 자주 변경되는 내용 중 하나이므로 다음과 같이 속성과 메서드를 제공하여 쉽게 변경할 수 있게 했다. 아이콘을 대체할 수 있는 속성과 메서드도 제공한다.

예제 11 레이아웃 정의 XML 파일

 

<com.handmark.pulltorefresh.library.PullToRefreshListView 
xmlns:ptr="http://schemas.android.com/apk/res-auto"
...
ptr:ptrPullLabel="잡아 당길 때의 레이블"
ptr:ptrRefreshLabel="새로 고칠 때의 레이블"
ptr:ptrReleaseLabel="새로 고침 완료 후 나타나는 레이블"
ptr:ptrDrawable="@drawable/icon"
/>

 

예제 12 메서드를 오버라이드하여 아이콘 정의

 

package com.example.yourproject; 
public final class CustomLoadingLayout extends FlipLoadingLayout {
public CustomLoadingLayout(Context context, Mode mode,
Orientation scrollDirection, TypedArray attrs) {
super(context, mode, scrollDirection, attrs);
}
@Override
protected String loadPullLabel(Context context, TypedArray attrs, Mode mode) {
return "Custom pull label";
}
@Override
protected String loadRefreshingLabel(Context context, TypedArray attrs, Mode mode) {
return "Custom refreshing label";
}
@Override
protected String loadReleaseLabel(Context context, TypedArray attrs, Mode mode) {
return "Custom release label";
}
/**
* Custom icon of loading layout
*/
@Override
protected int getDefaultDrawableResId() {
return R.drawable.icon;
}
}

 

Google 스타일 Pull To Refresh

Google이 최근에 Pull To Refresh를 이용하는 모든 앱에 적용한 새로운 스타일을 사용할 수 있게 했다.

Google 스타일의 Pull To Refresh UI는 기존의 Pull To Refresh UI에 적용된 바운스백(bounce back) 효과에 대한 특허 문제를 피하기 위해 고안된 UI이다. Google 스타일의 Pull To Refresh와 바운스백 특허에 대해서는 "Apple의 바운스백 특허 문제"에서 자세하게 설명하겠다.

Android Pull To Refresh 라이브러리 적용과 문제 해결

기존 앱에 Android Pull To Refresh 라이브러리 적용

위의 개선 사항들이 과연 기존 라이브러리의 문제를 해결할 수 있을까? 이를 검증하기 위해서 사내에서 개발한 앱 하나를 소스 코드로 받아 마이그레이션 작업을 수행했다.

마이그레이션을 수행한 앱은 Pull To Refresh 라이브러리의 소스 코드 전체를 복사한 후 프로젝트에 붙여 넣어 리패키징하고 필요한 부분을 수정해 사용하고 있었다. 게다가 Pull To Refresh 라이브러리의 버전도 최종 버전이 아닌 2.0.x 버전이었다. Android Pull To Refresh 라이브러리를 적용하기 전에 Pull To Refresh 라이브러리의 최종 버전으로 마이그레이션하는 중간 작업이 필요했다. 이 마이그레이션 작업에는 Android Pull To Refresh 라이브러리에 기록된 변경 로그가 도움이 되었다.

중간 작업 이후 Android Pull To Refresh 라이브러리를 적용하는 작업에 들어갔다. 먼저 무엇을 변경했는지 알아보았다. 조사한 바로는 많은 부분에 손댈 필요가 없어 보였다. 왜냐하면 이 앱도 로딩 레이아웃을 수정한 것 외에 크게 수정한 부분이 없기 때문이다. 레이블과 아이콘을 다른 것으로 대체한 정도이고 이를 위해 로딩 레이아웃 내에 API를 만들어 사용하고 있었다. 개선 사항에 이미 레이블 설정과 아이콘 설정이 있었으므로 완벽히 대체할 수 있었다.

CustomLoadingLayout 클래스를 만들어 assets/pulltorefresh.xml 파일에 추가하고 Pull To Refresh UI를 사용하는 부분에서는 레이블의 속성을 설정했다.

예제 13 assets/pulltorefresh.xml 파일

 

<?xml version="1.0" encoding="UTF-8"?> 
<PullToRefresh>
<LoadingLayouts>
<layout name="custom">com.navercorp.app.CustomLoadingLayout</layout>
</LoadingLayouts>
...
</PullToRefresh>

 

예제 14 수정한 로딩 레이아웃을 추가한 레이아웃 정의 XML 파일

 

<com.handmark.pulltorefresh.library.PullToRefreshWebView 
xmlns:ptr="http://schemas.android.com/apk/res-auto"
...
ptr:ptrAnimationStyle ="custom"
/>

 

이렇게 적용하니 이전과 동일하게 동작했다. 리패키징되어 있던 예전 라이브러리의 코드를 삭제해도 문제가 없었고 더 이상 라이브러리 내부에 손댈 필요도 없어졌다.

LoadingLayout의 inflate() 메서드 호출 문제

하지만 Android Pull To Refresh 라이브러리를 앱에 적용하고 나서 다른 문제를 발견할 수 있었다.

새로운 로딩 레이아웃을 만들려면 LoadingLayout 클래스를 오버라이드해야 한다. 그리고 새로운 로딩 레이아웃에 XML로 정의된 레이아웃 내용을 적용하고 싶다면 inflate() 메서드를 호출해야 한다. inflate() 메서드는 리소스의 layout 디렉터리에 있는 XML을 불러와 화면에 적용하는 역할을 한다.

하지만 새 로딩 레이아웃이 inflate() 메서드를 호출하더라도 내용이 제대로 적용되지 않았다. 이유는 부모 클래스(LoadingLayout)가 생성자에서 inflate() 메서드를 호출하였기 때문이다. 이미 다른 내용이 적용되어 있어서 나중에 호출한 내용이 올바르게 적용되지 않게 되었다.

이 문제를 피하려면 새로운 로딩 레이아웃에서 inflate() 메서드를 호출하기 전에 removeAllViews() 메서드를 호출해야 하는, 깔끔하지 않은 절차가 필요하다.

 

public class MyCustomLoadingLayout extends LoadingLayout { 
public MyCustomLoadingLayout
(Context context, final Mode mode, final Orientation scrollDirection, TypedArray attrs) {
super(context, mode, scrollDirection, attrs);
removeAllViews();
}
...
}

 

이 역시 클래스 관계를 정리하여 개선할 수 있지만 LoadingLayout 클래스의 역할이 다시 바뀌기 때문에 호환성을 생각하여 그대로 두었다. Android Pull To Refresh 라이브러리의 새로운 메이저 버전에서는 ILoadingLayout 클래스와 LoadingLayout 클래스의 역할을 정리해서 레이아웃을 자유롭게 정의하도록 지원할 계획이다.

기타 고려 사항

지금까지는 주요 개선 사항에 대해 다뤘다. 이제부터는 개선 사항 외적인 부분에 대해서 언급하고자 한다.

Apple의 바운스백 특허 문제

Android에서 사용하던 스크롤 기반 뷰는 Apple의 바운스백 관련 특허를 위반하는 문제를 안고 있었다. Android Pull To Refresh 라이브러리도 스크롤을 이용하기 때문에 이 문제를 무시할 수 없다. 여기에서는 Apple의 특허 문제를 언급하고, Android에서 특허 문제를 피할 수 있게 Android Pull To Refresh 라이브러리를 사용하는 방법을 설명하고자 한다.

트위터의 Pull To Refresh 특허 무료 전환

2010년 트위터는 Pull To Refresh UI에 대해 특허 출원을 신청한다. 이 출원 이후 Pull To Refresh UI를 사용할 수 없을 것이라는 우려와는 달리, 트위터는 UI 사용에 대한 특허권을 행사하지 않는다고 선언한다. 따라서 Pull To Refresh UI를 자유롭게 사용하는 부분에 있어서는 문제가 되지 않는다.

참고
트위터의 Pull To Refresh UI 특허권 행사에 대한 내용은 "
'Pull-to-refresh' 특허 무료전환, 이러니 사람들이 좋아하지?"에서 더 자세하게 살펴볼 수 있다.

Apple, 바운스백 특허로 삼성을 소송

Apple은 2011년 삼성을 대상으로 소송을 진행한다. 특허 위반 사실 중 하나가 바로 "rubber bend effect", "overscroll bounce" 혹은 "bounce-back effect"라 부르는 특허이다. 이는 다음 영상과 같이 내용의 끝으로 스크롤할 때 스크롤 범위를 벗어났다가(over scroll) 튕겨 나오는 효과이다.

82b99825ab22021ed9984f834a0cabf3.png

영상 1 바운스백 효과 동영상

2013년에 미국 특허청과 독일 법원이 Apple의 이 특허를 무효화하기도 했다. 하지만 Apple과의 소송 전쟁에서 삼성은 특허 위반 사실이 인정되어 패소와 함께 천문학적인 액수를 배상한다.

참고 

바운스백 특허에 관한 Apple과 삼성의 소송에 대해서는 다음 글들을 참고한다.
- "Apple Inc. v. Samsung Electronics Co., Ltd.", Wikipedia,http://en.wikipedia.org/wiki/Apple_Inc._v._Samsung_Electronics_Co.,_Ltd.
- "List scrolling and document translation, scaling, and rotation on a touch-screen display US 7469381 B2",http://www.google.com/patents/US7469381
- "미국 특허청, 애플 '바운스백' 사실상 무효 처리", 한국일보, 2013. 04. 02.,http://news.hankooki.com/lpage/economy/201304/h2013040221061121540.htm
- "German Court Uses Apple Video to Kill Bounce Back Patent", The Mac Observer, 2013. 09. 23.,http://www.macobserver.com/tmo/article/german-court-uses-apple-video-to-kill-bounce-back-patent
- "Apple's crucial overscroll bounce patent claim is valid, U.S. patent office says"m Macworld, 2013. 06. 02.,http://www.macworld.com/article/2042023/apples-crucial-overscroll-bounce-patent-claim-is-valid-us-patent-office-says.html

 

Pull To Refresh UI에서의 바운스백 효과

Android에서 사용하던 Pull To Refresh UI 역시 이 소송 결과의 영향을 피해 갈 수 없다. Pull To Refresh UI도 잡아당기는 영역에서 바운스백이 발생한다. 그리고 바운스백은 Pull To Refresh UI에 중요한 부분이다.

하지만 이대로 사용하기에는 Pull To Refresh 특허를 갖고 있는 트위터도 소송의 위험이 있는 것이 아닌가? Google도 더 이상 Pull To Refresh UI를 사용할 수 없고, 오픈소스인 Android Pull To Refresh 라이브러리를 사용하는 것에도 위험이 따른다.

참고
사용 가능 여부와는 별개로 Pull To Refresh UI가 Android에 어울리는 UI인지에 대한 논쟁도 있다. 이 논쟁에 대해서는 ""Pull-to-refresh": An Anti UI Pattern on Android"를 참고한다.

해결 방안

Apple의 소송을 피하기 위해 Google과 트위터는 각각 다른 방식을 사용한다.

Google은 최근에 Pull To Refresh UI를 이용하는 모든 앱을 새로운 스타일로 대체했다. 다음은 새로운 Google 스타일의 Pull To Refresh UI 영상이다.

1963cec531c01cc5c353c1a9f2966c08.png

영상 2 Google 스타일의 Pull To Refresh UI

위의 영상에서 보는 것과 같이 잡아당겨지는 상단 부분을 고정시켜, Pull To Refresh UI의 바운스백 효과를 없앴다. 이 스타일은 Android만의 트렌드가 될 것이고 대부분의 Android 앱은 이 스타일을 따라갈 것으로 보인다. Chris Banes도 기존의 Pull To Refresh 프로젝트 대신 새로운 Google 스타일이 적용된 ActionBar-PullToRefresh 프로젝트를 새로 개발하여 공개했다.

트위터는 기존의 Pull To Refresh UI의 동작과 비슷하지만, 평소에 스크롤할 때 잡아당겨지는 영역까지 스크롤하면 바운스백 효과가 발생하는 동작은 일어나지 않게 했다.

6013223c85d024a70f196c75245a3f34.png

영상 3 트위터의 Pull To Refresh

위 영상의 1:07 ~ 1:09 부분에서 이를 적용한 UI를 잠깐 볼 수 있다. 빠르게 스크롤하여 화면 끝까지 가면 스크롤이 화면의 끝에서 멈추는 것을 볼 수 있다. 이 외에는 예전의 Pull To Refresh UI와 같다고 보면 된다. Pull To Refresh UI의 특허권을 가지고 있는 트위터인 만큼, 어떻게 특허 분쟁을 피할 수 있는지도 잘 알고 있을 것이라 생각된다. 따라서 기존의 Pull To Refresh 방식을 유지할 것이라면 트위터 방식을 모델로 삼으면 될 것이다.

Android Pull To Refresh 라이브러리에서는 위에서 설명한 트위터 방식처럼 스크롤 시 발생하는 바운스백 효과를 비활성화시킬 수 있다. 방법은 간단하다. 다음과 같이 ptrOverScroll 속성을 false로 설정하면 된다.

예제 15 바운스백 효과를 없앤 레이아웃 정의 XML 파일

 

<com.handmark.pulltorefresh.library.PullToRefreshListView 
xmlns:ptr="http://schemas.android.com/apk/res-auto"
...
ptr:ptrOverScroll="false"
/>

 

여기에 더해서 Android Pull To Refresh 라이브러리는 간단하게 옵션만 설정하면 기존 Pull To Refresh UI 스타일을 Google 스타일의 Pull To Refresh UI로 변경할 수 있는 기능도 제공한다. 즉, Google 방식과 트위터 방식을 모두 지원한다.

그림 8 Android Pull To Refresh 라이브러리에서 Google 스타일 지원

오픈소스 공개

Android Pull To Refresh 라이브러리를 외부에 배포하기 위해 버전을 2.1에서 3.0으로 변경한 후, Chris Banes의 Pull To Refresh 프로젝트를 분기(fork)하여 새 GitHub 프로젝트로 관리하고 있다. 프로젝트를 관리하는 사이트의 주소는https://github.com/nhnopensource/android-pull-to-refresh이다. 현재 버전은 3.1이다.

영어 커밋 기록과 문서

외부 공개의 대상이 라이브러리를 사용하는 전 세계의 개발자이기 때문에 커밋 기록과 문서는 영어로 작성해야 했다. 프로젝트에 있는 위키 문서나 커밋 기록을 보면 영어로 작성되어 있는 것을 알 수 있다. 이슈 보고도 영어로 답변하고 있으니 이해해 주길 바란다.

Apache 라이선스

Chris Banes의 Pull To Refresh 라이브러리는 Apache 라이선스를 사용하고 있다. 라이선스에 대해 잘 알지 못해 Apache 라이선스에 대해서는 어떻게 저작권 표기를 하는지 자문을 받아서 알아냈다.

간단하게도, 소스 코드를 수정한 경우에는 원저작자와 자신을 함께 표기하고 추가한 소스에 대해서만 자신을 표기하여 라이선스를 처리했다.

 

/******************************************************************************* 
* Copyright 2011, 2012 Chris Banes.
* Copyright 2013 Naver Business Platform Corp.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/

 

Maven 중앙 저장소에 라이브러리 등록

Maven 사용자를 위해 Maven 중앙 저장소에 라이브러리를 등록하여 다음과 같이 쉽게 dependency를 추가할 수 있게 했다.

 

<dependencies> 
<dependency>
<groupId>com.navercorp.pulltorefresh</groupId>
<artifactId>library</artifactId>
<version>3.1.0</version>
<type>apklib</type>
</dependency>
<dependencies>

 

Maven 중앙 저장소에 라이브러리를 올리는 과정에서는 Sonatype이라는 오픈소스 저장소 호스팅(open source repository hosting) 서비스를 이용했다. 자체적으로 Maven 저장소를 제공하고 Maven 중앙 저장소와 연동시키는 서비스이다. 이 과정이 궁금하면 Sonatype에서 Maven 저장소를 사용하는 방법을 설명하는 "Sonatype OSS Maven Repository Usage Guide"를 참고한다.

마치며

지금까지 Pull To Refresh 라이브러리가 가진 문제점과 그 문제점을 개선해 공개한 Android Pull To Refresh 라이브러리에 대해 알아보았다.

ActionbarSherlock이라는 라이브러리가 있다. ActionBar가 Android 3.0 미만에서는 지원되지 않는 '빈틈'이 있었는데 이 부분을 채워 Android 2.x부터 지원되게 하자 엄청나게 인기를 끌었다(Android Support Library의 Revision 18부터 Actionbar가 포함되어 Android 2.1 이상에서 사용할 수 있다). Chris Banes의 Pull To Refresh 라이브러리도 마찬가지이다. Android에 Pull To Refresh UI 컴포넌트가 없는 빈틈을 채워주는 역할을 하며 대체 불가한 라이브러리가 되었다.

그런데 일 년 넘게 이 라이브러리가 방치되어 있다. 매우 안타까운 일이다. 아직도 제 역할을 하는 라이브러리이지만 더 이상 개선과 이슈 해결을 기대할 수 없다. 그래서 누군가가 대신해서 다시 시작해야 할 일이라 생각해서 이렇게 개선하는 작업을 시작했다.

이 글은 기술적인 노하우를 알려주거나 근사한 정보를 알려주는 글은 아니다. 단지 이 글을 통해 이 의도를 공유하고 많은 사람들이 사용했으면 하는 바람이 있다. 이제 개선 버전인 Android Pull To Refresh 라이브러리를 사용하면 문제가 발생하거나 원하는 기능이 있을 때 지원이 가능하다는 점을 상기해 주길 바란다.

개선은 이제 시작 단계이다. 많은 사용자가 생기고 다양한 개선을 요구하는 피드백이 활성화된다면 기능이 더 좋아지고 프로젝트가 더욱 활성화될 것이다. GitHub 프로젝트의 Issues를 클릭하면 언제든 문제나 요구 사항을 말할 수 있다.

또한 Pull Request도 환영한다. 갈수록 모든 개발자가 동등하게 개발하고 참여하는 프로젝트가 되었으면 한다.

네이버 비즈니스 플랫폼 웹플랫폼개발랩 김원준
뿔테 안경을 쓰고 체크무늬 남방에 청바지를 입는 평범한 개발자입니다.


네이버 비즈니스 플랫폼 웹플랫폼개발랩 정성학
갓 졸업한 풋풋한 신입사원입니다. 열심히 신입사원 교육을 받고 있습니다. 실력 있는 개발자가 되기 위해 노력하겠습니다. 지켜봐 주세요. 파이팅!