※ RecyclerView

: RecyclerView는 흔히들 우리가 사용하는 ListView나 GridView와 비슷하나 더 쉽고 더 유연하고 효율적이다. RecyclerView는 화면에 보여줄 View들을 재활용하고 화면에 보여주는 역할만 한다. 재활용한다는 뜻은 예를 통해 알아보자. 액티비티가 RecyclerView로 구성되어 있고 RecyclerView가 100개의 리스트 항목 View들을 보여준다고 가정하자. 휴대폰에서 해당 액티비티를 시작했을 때 휴대폰 화면 크기상 리스트 항목이 12개 밖에 안 보인다고 하면 사용자가 액티비티를 위 아래로 스크롤 할 시 나머지 리스트 항목이 보여질 것이다. 그렇다면 RecyclerView는 리스트 항목 100개에 대해 각각의 객체를 만들까? 그렇지 않다. RecyclerView는 리스트 항목 객체를 재활용한다. 리스트 항목 각각의 객체를 만드는 게 아닌 한 화면을 채우는 리스트 항목의 갯수만큼 객체를 만든다. 즉 100개가 아닌 12개를 만드는 것이다. 그리고 사용자가 화면을 스크롤할 때 만들어진 12개의 객체를 다시 사용하는 것이다. 이런 역할을 RecyclerView가 한다. 앞에서 말했듯이 RecyclerView는 리스트 항목 View들을 재활용하고 화면에 보여주는 역할만 한다. 따라서 RecyclerView를 제대로 구현하기 위해서, 리스트 항목 View를 만들고 값을 대입하는 등의 기능을 수행하기 위해서 Adapter와 ViewHolder 클래스에 대해서 알아야한다. 이에 대해서 알아보자.  


※ ViewHolder

:ViewHolder가 하는 일은 리스트 항목 하나의 View를 만들고 보존하는 일을 한다. 리스트 항목이 하나의 ImageView와 TextView로 구성되어 있다고 하면 다음과 같이 코드를 작성할 수 있다.

public class RecyclerHolder extends RecyclerView.ViewHolder{
	private TextView textView;
	private ImageView imageView;

	public RecyclerHolder(View view){
		super(view);
		textView = (TextView)view.findViewById(R.id.~);
		imageView = (ImageView)view.findViewById(R.id.~);
	}
}

RecyclerView.ViewHolder을 상속받는 클래스를 작성한다. 그리고 리스트 항목 하나의 View를 생성자에서 만든다. 생성자의 매개변수는 리스트 항목에 대한 레이아웃이다. 위와 같이 RecyclerView는 자신이 View 객체를 생성하지 않는다. Adapter를 통해서 ViewHolder 객체를 생성한다. ViewHolder 클래스 내부에는 itemView라는 필드가 있는 데 해당 필드로 View 객체를 가져온다. 포스트 앞 부분에서 RecyclerView에서 12개의 리스트 항목 View가 생성된다고 했는 데 실제 ViewHolder가 12개 생성되는 것이다. ViewHolder가 하는 일은 이게 끝이다. 리스트 항목 하나의 View를 만들고 보존한다.


※ Adapter

: 다시 말하지만 RecyclerView는 단지 RecyclerView 항목들을 재활용하고 보여주는 역할만 한다. 즉 자신이 ViewHolder을 생성하지 않는다. Adpater에게 그 일을 요청한다. Adapter는 필요한 ViewHolder 객체를 생성하고 데이터를 ViewHolder 객체와 결합하는 역할을 한다. 이에 대해 구체적으로 살펴보자. RecyclerView는 화면에 보여질 때 Adapter와 소통을 한다. 먼저 RecyclerView는 우리가 구현할 Adapter의 getItemCount() 메서드를 호출해서 RecyclerView에서 보여줘야하는 리스트 항목 총 갯수를 요청한다. 포스트 앞 부분의 예에서 전체 리스트 항목 100을 Adpater에게 요청하는 것이다. 그 다음에 RecyclerView는 Adapter의 onCreateViewHolder(ViewGroup, int) 메서드를 호출한다. onCreateViewHolder 메서드도 우리가 뒷 부분에서 구현할 예정이다. onCreateviewHolder가 실행되면 RecyclerView는 Adapter에게 ViewHolder 객체를 받는다. 그리고 RecyclerView는 onBindViewholder(ViewHolder, int)를 호출하면서 전에 Adapter에게 받았던 ViewHolder 객체와 리스트에서 해당 ViewHolder의 위치를 인자로 전달한다. Adapter는 인자로 받은 위치에 맞는 데이터를 찾은 후 그 것을 ViewHolder의 View에 결합하게 된다. 이 과정이 끝나면 RecyclerView는 하나의 리스트 항목 View를 화면에 위치시킨다. 즉 포스트 앞 부분 예제에서 12번 위 과정을 거치면 화면에 꽉 찰 리스트 항목 View들이 다 생성되는 것이다. 그 다음부터는 RecyclerView는 onCreateView를 호출하지 않는다. 기존에 생성된 ViewHolder 객체를 재사용하기 때문이다. 이처럼 RecyclerView는 항목 View들을 재활용시켜 시간과 메모리를 절약한다.  


※ RecyclerView 구현하기

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState) {
	.....

	mRecyclerView = (RecyclerView)view.findViewById(R.id.main_recycler_view);
	mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

	.....
}

프래그먼트에 RecyclerView 객체를 만들고 setLayoutManager을 호출한다. RecyclerView는 직접 화면에 리스트 항목들을 위치시키진 않는다. LayoutManager에게 해당 작업을 위임한다. LayoutManager는 리스트 항복 View들을 화면에 위치시키고 스크롤 동작을 처리한다. 따라서 무조건 setLayoutManager을 사용해야 한다. 인자로 LinearLayoutManager가 들어가면 ListView와 같이 보이고 GridLayoutManager을 전달하면 GridView처럼 보인다. StaggeredGridLayoutManager은 크기가 일정하지 않은 아이템을 격자 형태로 보여준다. 참고로 RecyclerView 자체 크기가 변하지 않을 때는 setHasFixedSize(ture)를 호출하면 성능이 향상된다. 다음은 Adapter 클래스이다.

class RecyclerAdapter extends RecyclerView.Adapter<RecyclerData> {
        private List<RecyclerData> mRecyclerData;

        public CrimeAdapter(List<RecyclerData> RecyclerData) {
            mRecyclerData = RecyclerData;
        }

        @Override
        public RecyclerDataHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater layoutInflater = LayoutInflater.from(getActivity());
            View view = layoutInflater
                    .inflate(R.layout.list_item_data, parent, false);
            return new RecyclerHolder(view);
        }

        @Override
        public void onBindViewHolder(CrimeHolder holder, int position) {
            RecyclerData recyclerData = mRecyclerData.get(position);
            holder.bindRecyclerData(recyclerData);
        }

        @Override
        public int getItemCount() {
            return mCrimes.size();
        }
}

위 코드를 보면 실제로 onCreateViewHolder에서 ViewHolder 객체를 생성하는 걸 볼 수 있다. onBindViewHolder 메서드를 보면 RecyclerView에서 받은 위치 값을 토대로 알맞은 데이터를 찾은 후 holder의 bindRecyclerData를 호출한다. 이 메서드는 다음 코드에서 볼 예정인데 ViewHolder 안 View에 데이터를 넣어주는 역할을 한다. getItemCount 메서드는 전체 리스트 항목의 갯수를 반환한다. 

private class RecyclerHolder extends RecyclerView.ViewHolder
            implements View.OnClickListener {
        private RecyclderData mRecyclderData;
        private TextView mTitleTextView;
        private TextView mDateTextView;
        private CheckBox mSolvedCheckBox;

        public RecyclerHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(this);

            mTitleTextView = (TextView)
                    itemView.findViewById(R.id.list_item_recycler_title_text_view);
            mDateTextView = (TextView)
                    itemView.findViewById(R.id.list_item_recycler_date_text_view);
            mSolvedCheckBox = (CheckBox)
                    itemView.findViewById(R.id.list_item_recycler_solved_check_box);

        }

        public void bindRecyclderData(RecyclderData recyclderData) {
            mRecyclderData = recyclderData;
            mTitleTextView.setText(mRecyclderData.getTitle());
            mDateTextView.setText(mRecyclderData.getDate().toString());
            mSolvedCheckBox.setChecked(mRecyclderData.isSolved());
        }

        @Override
        public void onClick(View v) {
            Toast.makeText(getActivity(),
                    RecyclerData.getTitle() + " 선택됨!", Toast.LENGTH_SHORT)
                    .show();
        }
}
그리고 마지막으로 RecyclerView와 Adpater, 리스트 항목에 들어갈 데이터를 연결해주는 작업만 하면 된다.


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_main_list, container, false);

        mRecyclerView = (RecyclerView) view
                .findViewById(R.id.main_recycler_view);
        mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));

        updateUI();

        return view;
    }

    @Override
    public void onResume(){
        super.onResume();
	updateUI();
    }

    private void updateUI() {
        List<RecyclerData> recyclerDatas = mRecyclerDatas

	if(mAdapter == null){
 	       mAdapter = new RecyclerAdapter(recyclerDatas);
 	       mRecyclerView.setAdapter(mAdapter);
	}else{
		mAdapter.notifyDataSetChanged();
	}
    }

updateUI 메서드를 보면 notifyDataSetChanged() 메서드가 있다. 해당 메서드는 리스트 항목 뷰에 들어간 데이터가 바꼈을 때 호출한다. 만약 현재 화면에 보이는 리스트 항목 View 한 개의 데이터가 바뀌어 다른 데이터를 출력해야 한다고 하면 notifyDataSetChanged() 메서드를 호출하면 된다. 그래서 onResume() 에서 updateUI를 호출한다. 다른 액티비티에서 데이터가 변경될 수 있기 때문이다. 그런데 onStart()가 아닌 onResume()에서 호출하는 이유는 다른 액티비티로 화면이 이동하고 다시 돌아오는 게 아닌 현재 RecyclerView 위에 투명 액티비티가 올라오면 일시정지가 될 수 있기 때문이다. 그러면 onStart는 호출되지 않을 것이다. 따라서 onResume()에서 호출했다. 추가로 notifyDataSetChanged()는 리스트 전체 데이터에 대해서 반응한다. 따라서 별도로 하나의 리스트 항목 View에 대해서 notifyDataSetChanged(int)를 구현하면 더 효율적일 것이다. 



- RecyclerView 구분선 표시하기

: RecyclerView에 구분선을 표시하기 위해서는 RecyclerView.ItemDecoration 클래스를 상속해 onDraw 메서드를 오버라이딩 하면 된다. getItemOffsets() 메서드로 각 아이템에 대한 Offset 빈 영역을 설정하고 onDraw()로 실제 구분선을 그린다. 아래와 같이 코딩한 후 recyclerView.addItemDecoration(new DividerItemDecoration(this));를 호출하면 된다.

public class DividerItemDecoration extends RecyclerView.ItemDecoration {

private final int dividerHeight;
private Drawable divider;

public DividerItemDecoration(Context context) {
// 기본인 ListView 구분선의 Drawable을 얻는다(구분선을 커스터마이징하고 싶을 때는 여기서 Drawable을 가져온다)
final TypedArray a = context.obtainStyledAttributes(new int[]{android.R.attr.listDivider});
divider = a.getDrawable(0);

// 표시할 때마다 높이를 가져오지 않아도 되게 여기서 구해 둔다
dividerHeight = divider.getIntrinsicHeight();
a.recycle();
}

// View의 아이템보다 위에 그리고 싶을 때는 이쪽 메소드를 사용한다
// @Override
// public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
// super.onDrawOver(c, parent, state);
// }


// View의 아이템보다 아래에 그리고 싶을 때는 이쪽 메소드를 사용한다
// 여기서는 RecyclerView의 아이템마다 아래에 선을 그린다
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
// 좌우의 padding으로 선의 right과 left를 설정
final int lineLeft = parent.getPaddingLeft();
final int lineRight = parent.getWidth() - parent.getPaddingRight();

final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();

// 애니메이션 등일 때에 제대로 이동하기 위해서
int childTransitionY = Math.round(ViewCompat.getTranslationY(child));
final int top = child.getBottom() + params.bottomMargin + childTransitionY;
final int bottom = top + dividerHeight;

// View 아래에 선을 그린다
divider.setBounds(lineLeft, top, lineRight, bottom);
divider.draw(c);
}
}

@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
// View 아래에 구분선이 들어가므로 아래에 Offset을 넣는다
outRect.set(0, 0, 0, dividerHeight);
}
}


+ Recent posts