본문 바로가기

안드로이드 개발 팁/UI

RecyclerView에서 스크롤바를 표시하는 방법

RecyclerView를 사용한 UI를 제작하던 중, XML레이아웃을 사용하지 않고 직접 코드 상에서 RecyclerView의 인스턴스를 생성하여 사용할 일이 생겼습니다. 그런데, 스크롤바를 표시하도록 설정하는 과정에서 문제가 발생했습니다. setVerticalScrollBarEnabled() 메서드를 사용하면 되리라 예상했는데, 스크롤바는 모습을 드러내지 않았습니다. (..)


왠지 같은 문제를 겪고 있는 사람들이 많을 듯 해서 구글링을 해보니 아니나 다를까, 질문과 답변 모두 있네요. (Stack Overflow)

결론만 정리하자면, 스크롤바를 표시를 위한 초기 작업이 XML을 통해 뷰가 초기화 될 때만 수행되어 직접 코드를 사용하여 초기화 할 때에는 위의 메서드가 제대로 동작하지 않았던 겁니다.


조금 더 상세히 들어가자면, 코드 상에서 새로운 뷰를 생성할 떄 사용하는 생성자에선 스크롤바 표시를 위한 초기화가 진행되지 않고, XML을 통해 뷰가 생성될 때 사용하는 생성자에서만 초기화가 진행되어 스크롤바를 정상적으로 표시할 수 있습니다.


public RecyclerView(Context context)  // 스크롤바를 표시할 수 없음

public RecyclerView(Context context, AttributeSet attrs) // 스크롤바 표시 가능

public RecyclerView(Context context, AttributeSet attrs, int defStyle) // 스크롤바 표시 가능


따라서, 현재로선 스크롤바를 표시할 수 있는 RecyclerView의 인스턴스를 자바 코드 상에서 사용하려면 XML 레이아웃을 사용하여 뷰를 선언한 다음, LayoutInflater를 사용하여 뷰의 인스턴스를 받아야 합니다.


먼저, 다음과 같이 XML 레이아웃을 생성한 후, RecyclerView를 선언합니다. XML을 사용하는 김에, 속성에 스크롤바 표시 속성도 함께 추가합니다.


[recycler_view.xml]

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scrollbars="vertical"/>


다음, 코드 상에서 LayoutInflater를 사용하여 위에서 생성한 레이아웃을 불러오면 됩니다.

RecyclerView v = (RecyclerView) LayoutInflater.from(context)
        .inflate(R.layout.recycler_view, parent, false);


다음은 Fragment의 onCreateView()에서 위에서 정의한 RecyclerView를 사용하는 예를 보여줍니다.

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
        Bundle savedInstanceState) {
    return inflater.inflate(R.layout.recycler_view, container, false);
}


스택오버플로우를 보고 궁금해서 소스 코드를 찾아보니, 스크롤바를 표시할 수 있도록 초기화 하는 부분은 다음 부분입니다.


[platform/frameworks/base/core/java/android/view/View.java]

protected void initializeScrollbarsInternal(TypedArray a) {
    initScrollCache();
    final ScrollabilityCache scrollabilityCache = mScrollCache;
    if (scrollabilityCache.scrollBar == null) {
        scrollabilityCache.scrollBar = new ScrollBarDrawable();
    }
    final boolean fadeScrollbars = a.getBoolean(R.styleable.View_fadeScrollbars, true);
    if (!fadeScrollbars) {
        scrollabilityCache.state = ScrollabilityCache.ON;
    }
    scrollabilityCache.fadeScrollBars = fadeScrollbars;
    scrollabilityCache.scrollBarFadeDuration = a.getInt(
            R.styleable.View_scrollbarFadeDuration, ViewConfiguration
                    .getScrollBarFadeDuration());
    scrollabilityCache.scrollBarDefaultDelayBeforeFade = a.getInt(
            R.styleable.View_scrollbarDefaultDelayBeforeFade,
            ViewConfiguration.getScrollDefaultDelay());
    scrollabilityCache.scrollBarSize = a.getDimensionPixelSize(
            com.android.internal.R.styleable.View_scrollbarSize,
            ViewConfiguration.get(mContext).getScaledScrollBarSize());
    Drawable track = a.getDrawable(R.styleable.View_scrollbarTrackHorizontal);
    scrollabilityCache.scrollBar.setHorizontalTrackDrawable(track);
    Drawable thumb = a.getDrawable(R.styleable.View_scrollbarThumbHorizontal);
    if (thumb != null) {
        scrollabilityCache.scrollBar.setHorizontalThumbDrawable(thumb);
    }
    boolean alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawHorizontalTrack,
            false);
    if (alwaysDraw) {
        scrollabilityCache.scrollBar.setAlwaysDrawHorizontalTrack(true);
    }
    track = a.getDrawable(R.styleable.View_scrollbarTrackVertical);
    scrollabilityCache.scrollBar.setVerticalTrackDrawable(track);
    thumb = a.getDrawable(R.styleable.View_scrollbarThumbVertical);
    if (thumb != null) {
        scrollabilityCache.scrollBar.setVerticalThumbDrawable(thumb);
    }
    alwaysDraw = a.getBoolean(R.styleable.View_scrollbarAlwaysDrawVerticalTrack,
            false);
    if (alwaysDraw) {
        scrollabilityCache.scrollBar.setAlwaysDrawVerticalTrack(true);
    }
    // Apply layout direction to the new Drawables if needed
    final int layoutDirection = getLayoutDirection();
    if (track != null) {
        track.setLayoutDirection(layoutDirection);
    }
    if (thumb != null) {
        thumb.setLayoutDirection(layoutDirection);
    }
    // Re-apply user/background padding so that scrollbar(s) get added
    resolvePadding();
}


위 메서드는 아래 메서드에서 호출되는데요,


[platform/frameworks/base/core/java/android/view/View.java]

protected void initializeScrollbars(TypedArray a) {
    // It's not safe to use this method from apps. The parameter 'a' must have been obtained
    // using the View filter array which is not available to the SDK. As such, internal
    // framework usage now uses initializeScrollbarsInternal and we grab a default
    // TypedArray with the right filter instead here.
    TypedArray arr = mContext.obtainStyledAttributes(com.android.internal.R.styleable.View);
    initializeScrollbarsInternal(arr);
    // We ignored the method parameter. Recycle the one we actually did use.
    arr.recycle();
}

initializeScrollbars() 메서드가 호출되려면 View 클래스 내 initializeScrollbars 필드가 true로 설정되어야 합니다. 그런데, 이 필드를 true로 설정하는 방법은 생성자에서 Attribute를 읽어 설정할 때, 즉 XML을 통해 뷰를 생성할 때 밖에 없습니다. 아래는 initializeScrollbars 필드를 설정하는 생성자 코드 일부입니다.


[platform/frameworks/base/core/java/android/view/View.java]

case R.styleable.View_scrollbars:
    final int scrollbars = a.getInt(attr, SCROLLBARS_NONE);
    if (scrollbars != SCROLLBARS_NONE) {
        viewFlagValues |= scrollbars;
        viewFlagMasks |= SCROLLBARS_MASK;
        initializeScrollbars = true;
    }
    break;

위와 같이 필드값을 설정한 후에 최종적으로 아래 코드가 호출되며 초기화 작업이 수행됩니다.


[platform/frameworks/base/core/java/android/view/View.java]

if (initializeScrollbars) {
    initializeScrollbarsInternal(a);
}


코드를 자세히 보진 않았지만, 충분히 뷰 초기화 이후에도 스크롤바를 표시할 수 있도록 초기화 작업을 하는 것엔 무리가 없을 것 같은데, 의도적으로 이런 식으로 구성이 되어 있는 것인지, 아니면 버그인지는 조금 더 살펴보아야 겠습니다. 당분간은 위에서 설명한 임시방편을 사용해야 할 것 같네요.