구글플러스 안드로이드 앱 프로필 화면 구성


요즘 안드로이드 UI를 보면 섹션바가 중간쯤 있다가 위로 올라가면 위쪽에 걸쳐지는 경우가 많다. 구글에서 만들 앱을 보면 더더욱 많이 쓰고 있다. 구글 플러스의 프로필 페이지와 얼마전에 업데이트된 뉴스 스텐드 앱이다.

하지만 자세히보면 모두 구현 방식이 다르다.

구글 플러스

구글 뉴스 스텐드, Airbnb

  • ViewPager를 사용하여 좌우 스와이프가 됨.
  • ViewPager뒤에 배경을 주고 페이지별로 상단에 여백을 주어 배경이 비치게한뒤, 스크롤시 여백을 줄이는지 리스트뷰 터치이벤트를 넘겨 스크롤될지 판단하는 로직.

구글 플러스 프로필 화면 뷰 구조

프로필 화면은 리스트뷰로 이루어 져있으며, 초록색은 Adapter의 아이템이고, 주황색과 빨강색은 헤더뷰로 구성되었다. OnScrollListener을 통해서 스크를을 콜백 받을 수 있는데, 섹션뷰의 위치가 최상단에 오게 되면 섹션뷰를 리스트뷰위에 그리는 방식이다.

스크롤 도중에 프로필이미지의 위치도 Translation Animation을 이용해서 변경한다.

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
    int totalItemCount) {
  if(visibleItemCount == 0) return;
  if(firstVisibleItem != 0) return;

  mImageView.setTranslationY(-mListView.getChildAt(0).getTop() / 2);

}

섹션뷰를 리스트뷰에 그리기위해서는 리스트뷰를 상속받아서 dispatchDraw()에서 canvas에 뷰를 그려주면 된다.

mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
    //...

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        if (visibleItemCount == 0) return;

        boolean shouldStick = false;
        if (view.getChildAt(0) != mHeader) shouldStick = true;
        else {
            int top = mHeader.getTop();
            int tabsHeight = mHorizontalScrollView.getMeasuredHeight();
            int headerViewHeight = mHeader.getMeasuredHeight();
            int delta = headerViewHeight - tabsHeight;

            if (delta + top < 0) shouldStick = true;
        }

        //sticky header
        if (shouldStick)
            mListView.setViewToDraw(mHorizontalScrollViewContainer);
        else
            mListView.setViewToDraw(null);
    }
});

이렇게 그리는것 외에 터치했을때 뷰가 터치이벤트를 받아야 하기때문에 dispatchTouchEvent()에서 처리해준다. 리스트뷰위에 그리면 이처럼 터치이벤트에 대한 처리를 따로 해줘야 하지만, 리스트위에 레이아웃을 미리구성해놓고 섹션뷰가 최상단에 오면 Visibility속성을 이용해서 보이거나 숨기하면 따로 터치이벤트 처리는 하지 않아도 된다. 하지만 리스트뷰위에 필요 없이 레이아웃이 한번더 그려지는 불필요 한 상황이 발생된다.

구글 플러스 방식은 하나의 리스트뷰로 섹션변경시 Adapter을 변경하는 방식이라 어떻게 보면 뷰 구조가 가벼울 수 있다. 하지만 좌우 스와이프기능이 안되기 때문에 불편 할 수도 있다.

뉴스 스텐드 구현 방식을 보자.

뷰 구조를 보면 ViewPager와 섹션기능을 하는 탭 호스트로 나누어 진다. 정확하게 파악 할 수는 없지만 상단 이미지를 ViewPager뒤쪽에 깔아 놓고 리스트뷰의 터치이벤트를 통해서 빈공간의 높이를 변경 하는 방식인것 같다.

 

빈공간은 단순히 뷰의 페딩이나 마진값으로 높이는 조절 할 수도 있고, 스크롤뷰를 통해 터치이벤트를 계산하여 스크롤 되는 다양한 방식이 있을 것이다. Airbnb 앱을 보면 구글 뉴스스텐드와 동인한 UI가 있는데, 여기 개발자도 만들면서 다양한 경험을 공유(http://nerds.airbnb.com/host-experience-android)하고 있다.

 

이는 구글 플러스 방식 보다 구현이 더 힘들다. 그 이유는 리스트뷰가 스크롤되어야 할때와 섹션헤더가 줄어들어야 할때를 이벤트 처리 해야 하기 때문이다. 예를 들면 섹션헤더가 줄어들다가 완전히 줄어든 경우 이벤트가 리스트뷰로 들어가서 스크롤이 되어야 하는등의 처리 방식에서 이벤트가 꼬이는 문제가 생길가능성이 높기 때문이다.

 

 

Airbnb는 이렇게 해결했다고 한다. ViewPager와 섹션, 상단 이미지를 화면크기보다 더 크게 그려 놓고, 사용자가 스크롤 하는 시점에 상단이미지의 높이를 계산해서 0이 아니면 상단 이미지를 계속 줄여 나가게 된다. 0이 되면 ViewPager와 섹션이 화면크기게 정확하게 맞아지고, 이때 부터는 터치이벤트가 ViewPager에서 먹혀서 리스트뷰나 스크롤뷰가 스크롤가능하게 된다. 반대로 상단에서 아래로 끌때는 스크롤뷰나 리스트뷰가 최상단의 스크롤된 위치인가는 확인하고 최상단이면 상단이미지를 늘리게 되는 구조이다.

ViewPager이기때문에 페이지가 변경될때마다 상단 이미지의 높이를 이전페이지와 유지하기위한 작업도 수행한다.

정리하자면 구글 플러스 방식은 개발하기 훨씬 쉽고 사용하기 불편하다. 뉴스 스텐드 방식은 개발하기 훨씬 어렵고 사용하기 편하다. 여러분의 선택은?




Android ViewPager 성능향상 방법

안드로이드 초기에만 해도 Activity는 하나의 페이지로만 인식이 되었는데, 몇년전부터 하나의 Activity에서도 여러개의 화면을 배치할 수 있는 UI들이 생겨나고 있다. 또한 폰뿐만 아니라 테블릿에서도 다양한 UI를 위해 분할된 화면들로 배치하고 있다. 이런 UI중심에는 ViewPager라는 안드로이드에서 지원하는 View가 있다.

ViewPager은 화면전환 없이 좌우 스크롤을 통해 효율적으로 페이지 전환을 할 수 있는 UI이다. ViewPager은 기본적으로 좌/우 화면을 미리 로딩해두기 때문에 좌우 스크롤시 빠른화면전환을 보여준다. 스크롤을 양옆으로 조금 당여보면 미리로딩되어 있다는 것을 알 수 있다.

 

하지만 이러한 로딩방식으로 처음 진입시 성능이나 메모리적으로 문제가 된다. 한 화면에 하나를 표시 할 것을 좌우 페이지를 미리불러오기 때문이다. 그래서 이 화면을 그리는 것에 대해 성능을 향상 하는것이 ViewPager의 성능향상이 되는 것이다.

1. View계층 단순화 및 thread분리

ViewPager는 ListView 처럼 View를 재활용 하는 방식이 아니라 보이는 페이지, 좌, 우 페이지가 필요 한경우 그때그때 View를 그리는 방식이다. 이 View를 초기화 할때 mainThread를 사용하는데, 이외 의 작업들은 다른 Thread를 이용해서 작업 해야한다. 만일 View초기화시 파일을 읽는다거나, 네트워크 작업을 하는등 mainThread에서 작업하게 된다면 페이지 전환이 엄청 느릴것이다. View의 계층 구조또한 단순히 해서 View를 그릴때 시간을 조금이라도 줄여야 한다.

 

2. 화면 그리는 시점 변경

1) ViewPager 로딩 방식을 변경

ViewPager은 기본적으로 현재 보고있는 페이지, 좌/우페이지로 3개의 ChildView를 미리 로딩한다. 이렇게 미리로딩 하는 구조가 아닌 현재 화면에 보이고 있는 로딩하는 구조로 변경하면 성능을 조금더 향상된다.  ViewPager의 OnPageChangeListener를 통해서 페이지가 변경되었을때 onPageScrollStateChanged으로 변경된 페이지Position을 CallBack받을 수 있다. 성능은 좋아지겠지만 화면이 전환 되었을때 로딩 하는 구조이기 때문에 사용자는 로딩을 보면서 기다리게 만들 수 있다는 단점이 존재한다.

mPager.setOnPageChangeListener(new OnPageChangeListener() {
    
    @Override
    public void onPageSelected(int state) {
        
    }
    
    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        
    }
    
    @Override
    public void onPageScrollStateChanged(int position) {
        
    }
});
2) PageLimit 갯수 변경

초반에 미리 로드해두는 방식이다. 예를 들어 페이지가 4개가 있을 경우 ViewPager는 초반에 1,2 페이지만 그리게 된다. 하지만 3페이지로 이동하면 3,4페이지가 로딩이 된다. 동시에 2개가 로딩되면서 버벅일 수 있다. 이렇게 페이지 이동시마다 많은 View들이 로딩을 하게 되면 무거워 질 수밖에 없다. 이렇게 페이지이동시마다 화면의 버벅임을 없애기 좋은 방법은 미리 모든 페이지를 로드해두는 것이다. 페이지가 많고 복잡한 구조이면 메모리문제가 생길 수 있으며, 4~5개의 간단한 페이지의 경우 미리 로딩해둔 다음 페이지만 이동하는 방식으로 바꾸는 것이 성능면에서 유리하다.

ViewPager의 setOffscreenPageLimit(int) 메서드를 이용하면 양쪽에 유지되는 페이지수를 설정 할 수가 있다. 예를 들어 4개의 페이지일 경우 3로 설정 하는 경우 4개의 페이지를 초반에 미리로딩한다. 페이지를 이동할때 마다 View를 지우고 새로만드는 작업은 하지않게 된다. 4개의 페이지가 미리로딩 되어 있기때문이다.

mPager.setOffscreenPageLimit(3);

참고: ViewPager View 강제로 다시그리기

위 3가지의 방법은 ViewPager가 어떤식으로 활용하는지에 따라 모두 성능이 다르다르기 때문에 적절하게 선택해서 쓰기바란다.