[java] Android 5.0-RecyclerView에 머리글 / 바닥 글 추가

에 헤더를 추가하는 방법을 알아 내려고 잠시 시간을 보냈습니다 RecyclerView.

이것이 내가 지금까지 얻은 것입니다.

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...

    layouManager = new LinearLayoutManager(getActivity());
    recyclerView.setLayoutManager(layouManager);

    LayoutInflater inflater = (LayoutInflater) getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    headerPlaceHolder = inflater.inflate(R.layout.view_header_holder_medium, null, false);
    layouManager.addView(headerPlaceHolder, 0);

   ...
}

LayoutManager의 처분 처리 대상이 될 것으로 보인다 RecyclerView항목을. addHeaderView(View view)메서드를 찾을 수 없어서 LayoutManageraddView(View view, int position)메서드를 사용하여 첫 번째 위치에 헤더보기를 추가하여 헤더 역할을하기로 결정했습니다.

Aaand 이것은 상황이 더 나빠지는 곳입니다.

java.lang.NullPointerException: Attempt to read from field 'android.support.v7.widget.RecyclerView$ViewHolder android.support.v7.widget.RecyclerView$LayoutParams.mViewHolder' on a null object reference
    at android.support.v7.widget.RecyclerView.getChildViewHolderInt(RecyclerView.java:2497)
    at android.support.v7.widget.RecyclerView$LayoutManager.addViewInt(RecyclerView.java:4807)
    at android.support.v7.widget.RecyclerView$LayoutManager.addView(RecyclerView.java:4803)
    at com.mathieumaree.showz.fragments.CategoryFragment.setRecyclerView(CategoryFragment.java:231)
    at com.mathieumaree.showz.fragments.CategoryFragment.access$200(CategoryFragment.java:47)
    at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:201)
    at com.mathieumaree.showz.fragments.CategoryFragment$2.success(CategoryFragment.java:196)
    at retrofit.CallbackRunnable$1.run(CallbackRunnable.java:41)
    at android.os.Handler.handleCallback(Handler.java:739)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:135)
    at android.app.ActivityThread.main(ActivityThread.java:5221)
    at java.lang.reflect.Method.invoke(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:372)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:899)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:694)

활동 생성의 다른 순간에 여러 번 NullPointerExceptions호출하려고 시도한 후 addView(View view)(모든 것이 설정되면 뷰를 추가하려고 시도했습니다. 어댑터의 데이터도) 이것이 올바른 방법인지 모르겠다는 것을 깨달았습니다 (그리고 그것은 보이지 않습니다).

PS : 또한, 처리 할 수있는 솔루션 GridLayoutManager받는 사람뿐만 아니라이 LinearLayoutManager정말 감사하겠습니다!



답변

바닥 글을 추가해야 RecyclerView했고 여기에서 유용 할 것이라고 생각한 코드 조각을 공유하고 있습니다. 전체 흐름을 더 잘 이해하려면 코드 내부의 주석을 확인하십시오.

import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import java.util.ArrayList;

public class RecyclerViewWithFooterAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    private static final int FOOTER_VIEW = 1;
    private ArrayList<String> data; // Take any list that matches your requirement.
    private Context context;

    // Define a constructor
    public RecyclerViewWithFooterAdapter(Context context, ArrayList<String> data) {
        this.context = context;
        this.data = data;
    }

    // Define a ViewHolder for Footer view
    public class FooterViewHolder extends ViewHolder {
        public FooterViewHolder(View itemView) {
            super(itemView);
            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the item
                }
            });
        }
    }

    // Now define the ViewHolder for Normal list item
    public class NormalViewHolder extends ViewHolder {
        public NormalViewHolder(View itemView) {
            super(itemView);

            itemView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // Do whatever you want on clicking the normal items
                }
            });
        }
    }

    // And now in onCreateViewHolder you have to pass the correct view
    // while populating the list item.

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        View v;

        if (viewType == FOOTER_VIEW) {
            v = LayoutInflater.from(context).inflate(R.layout.list_item_footer, parent, false);
            FooterViewHolder vh = new FooterViewHolder(v);
            return vh;
        }

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

        NormalViewHolder vh = new NormalViewHolder(v);

        return vh;
    }

    // Now bind the ViewHolder in onBindViewHolder
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

        try {
            if (holder instanceof NormalViewHolder) {
                NormalViewHolder vh = (NormalViewHolder) holder;

                vh.bindView(position);
            } else if (holder instanceof FooterViewHolder) {
                FooterViewHolder vh = (FooterViewHolder) holder;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // Now the critical part. You have return the exact item count of your list
    // I've only one footer. So I returned data.size() + 1
    // If you've multiple headers and footers, you've to return total count
    // like, headers.size() + data.size() + footers.size()

    @Override
    public int getItemCount() {
        if (data == null) {
            return 0;
        }

        if (data.size() == 0) {
            //Return 1 here to show nothing
            return 1;
        }

        // Add extra view to show the footer view
        return data.size() + 1;
    }

    // Now define getItemViewType of your own.

    @Override
    public int getItemViewType(int position) {
        if (position == data.size()) {
            // This is where we'll add footer.
            return FOOTER_VIEW;
        }

        return super.getItemViewType(position);
    }

    // So you're done with adding a footer and its action on onClick.
    // Now set the default ViewHolder for NormalViewHolder

    public class ViewHolder extends RecyclerView.ViewHolder {
        // Define elements of a row here
        public ViewHolder(View itemView) {
            super(itemView);
            // Find view by ID and initialize here
        }

        public void bindView(int position) {
            // bindView() method to implement actions
        }
    }
}

위의 코드 스 니펫은 RecyclerView. 이 GitHub 저장소 에서 머리글과 바닥 글을 모두 추가하는 구현을 확인할 수 있습니다 .


답변

해결하기 매우 간단합니다 !!

보기를 반환하기 전에보기 유형을 확인할 때마다 어댑터 내부에 논리를 다른보기 유형으로 사용하는 것은 마음에 들지 않습니다. 아래 솔루션은 추가 검사를 피합니다.

android.support.v4.widget.NestedScrollView 안에 LinearLayout (수직) 헤더 뷰 + recyclerview + 푸터 뷰를 추가하기 만하면 됩니다.

이것 좀 봐:

 <android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

       <View
            android:id="@+id/header"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:layoutManager="LinearLayoutManager"/>

        <View
            android:id="@+id/footer"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

부드러운 스크롤을 위해이 코드 줄을 추가하십시오.

RecyclerView v = (RecyclerView) findViewById(...);
v.setNestedScrollingEnabled(false);

이것은 모든 RV의 성능을 잃게됩니다 및 RV에 관계없이 모든 뷰 홀더를 배치하려고합니다 layout_heightRV의

Nav 서랍 또는 설정 등과 같은 작은 크기 목록에 사용하는 것이 좋습니다.


답변

Lollipop에서 동일한 문제가 발생하여 Recyclerview어댑터 를 감싸는 두 가지 방법을 만들었습니다 . 하나는 사용하기 매우 쉽지만 데이터 세트가 변경되면 어떻게 작동할지 모르겠습니다. 어댑터를 래핑 notifyDataSetChanged하고 올바른 어댑터 개체 와 같은 메서드를 호출해야하기 때문 입니다.

다른 사람에게는 그런 문제가 없어야합니다. 일반 어댑터가 클래스를 확장하고 추상 메서드를 구현하면 준비가 완료됩니다. 그리고 여기에 있습니다.

요점

HeaderRecyclerViewAdapterV1

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * This is a Plug-and-Play Approach for adding a Header or Footer to
 * a RecyclerView backed list
 * <p/>
 * Just wrap your regular adapter like this
 * <p/>
 * new HeaderRecyclerViewAdapterV1(new RegularAdapter())
 * <p/>
 * Let RegularAdapter implement HeaderRecyclerView, FooterRecyclerView or both
 * and you are ready to go.
 * <p/>
 * I'm absolutely not sure how this will behave with changes in the dataset.
 * You can always wrap a fresh adapter and make sure to not change the old one or
 * use my other approach.
 * <p/>
 * With the other approach you need to let your Adapter extend HeaderRecyclerViewAdapterV2
 * (and therefore change potentially more code) but possible omit these shortcomings.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public class HeaderRecyclerViewAdapterV1 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    private final RecyclerView.Adapter mAdaptee;


    public HeaderRecyclerViewAdapterV1(RecyclerView.Adapter adaptee) {
        mAdaptee = adaptee;
    }

    public RecyclerView.Adapter getAdaptee() {
        return mAdaptee;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER && mAdaptee instanceof HeaderRecyclerView) {
            return ((HeaderRecyclerView) mAdaptee).onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER && mAdaptee instanceof FooterRecyclerView) {
            return ((FooterRecyclerView) mAdaptee).onCreateFooterViewHolder(parent, viewType);
        }
        return mAdaptee.onCreateViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER && useHeader()) {
            ((HeaderRecyclerView) mAdaptee).onBindHeaderView(holder, position);
        } else if (position == mAdaptee.getItemCount() && holder.getItemViewType() == TYPE_FOOTER && useFooter()) {
            ((FooterRecyclerView) mAdaptee).onBindFooterView(holder, position);
        } else {
            mAdaptee.onBindViewHolder(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = mAdaptee.getItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    private boolean useHeader() {
        if (mAdaptee instanceof HeaderRecyclerView) {
            return true;
        }
        return false;
    }

    private boolean useFooter() {
        if (mAdaptee instanceof FooterRecyclerView) {
            return true;
        }
        return false;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == mAdaptee.getItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (mAdaptee.getItemCount() >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return mAdaptee.getItemViewType(position) + TYPE_ADAPTEE_OFFSET;
    }


    public static interface HeaderRecyclerView {
        public RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

        public void onBindHeaderView(RecyclerView.ViewHolder holder, int position);
    }

    public static interface FooterRecyclerView {
        public RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

        public void onBindFooterView(RecyclerView.ViewHolder holder, int position);
    }

}

HeaderRecyclerViewAdapterV2

import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;

/**
 * Created by sebnapi on 08.11.14.
 * <p/>
 * If you extend this Adapter you are able to add a Header, a Footer or both
 * by a similar ViewHolder pattern as in RecyclerView.
 * <p/>
 * If you want to omit changes to your class hierarchy you can try the Plug-and-Play
 * approach HeaderRecyclerViewAdapterV1.
 * <p/>
 * Don't override (Be careful while overriding)
 * - onCreateViewHolder
 * - onBindViewHolder
 * - getItemCount
 * - getItemViewType
 * <p/>
 * You need to override the abstract methods introduced by this class. This class
 * is not using generics as RecyclerView.Adapter make yourself sure to cast right.
 * <p/>
 * TOTALLY UNTESTED - USE WITH CARE - HAVE FUN :)
 */
public abstract class HeaderRecyclerViewAdapterV2 extends RecyclerView.Adapter {
    private static final int TYPE_HEADER = Integer.MIN_VALUE;
    private static final int TYPE_FOOTER = Integer.MIN_VALUE + 1;
    private static final int TYPE_ADAPTEE_OFFSET = 2;

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        if (viewType == TYPE_HEADER) {
            return onCreateHeaderViewHolder(parent, viewType);
        } else if (viewType == TYPE_FOOTER) {
            return onCreateFooterViewHolder(parent, viewType);
        }
        return onCreateBasicItemViewHolder(parent, viewType - TYPE_ADAPTEE_OFFSET);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && holder.getItemViewType() == TYPE_HEADER) {
            onBindHeaderView(holder, position);
        } else if (position == getBasicItemCount() && holder.getItemViewType() == TYPE_FOOTER) {
            onBindFooterView(holder, position);
        } else {
            onBindBasicItemView(holder, position - (useHeader() ? 1 : 0));
        }
    }

    @Override
    public int getItemCount() {
        int itemCount = getBasicItemCount();
        if (useHeader()) {
            itemCount += 1;
        }
        if (useFooter()) {
            itemCount += 1;
        }
        return itemCount;
    }

    @Override
    public int getItemViewType(int position) {
        if (position == 0 && useHeader()) {
            return TYPE_HEADER;
        }
        if (position == getBasicItemCount() && useFooter()) {
            return TYPE_FOOTER;
        }
        if (getBasicItemType(position) >= Integer.MAX_VALUE - TYPE_ADAPTEE_OFFSET) {
            new IllegalStateException("HeaderRecyclerViewAdapter offsets your BasicItemType by " + TYPE_ADAPTEE_OFFSET + ".");
        }
        return getBasicItemType(position) + TYPE_ADAPTEE_OFFSET;
    }

    public abstract boolean useHeader();

    public abstract RecyclerView.ViewHolder onCreateHeaderViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindHeaderView(RecyclerView.ViewHolder holder, int position);

    public abstract boolean useFooter();

    public abstract RecyclerView.ViewHolder onCreateFooterViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindFooterView(RecyclerView.ViewHolder holder, int position);

    public abstract RecyclerView.ViewHolder onCreateBasicItemViewHolder(ViewGroup parent, int viewType);

    public abstract void onBindBasicItemView(RecyclerView.ViewHolder holder, int position);

    public abstract int getBasicItemCount();

    /**
     * make sure you don't use [Integer.MAX_VALUE-1, Integer.MAX_VALUE] as BasicItemViewType
     *
     * @param position
     * @return
     */
    public abstract int getBasicItemType(int position);

}

피드백과 포크에 감사드립니다. 나는 HeaderRecyclerViewAdapterV2내 자신 이 사용 하고 향후 변경 사항을 진화, 테스트 및 게시 할 것입니다.

편집 : @OvidiuLatcu 예, 몇 가지 문제가 있습니다. 실제로 헤더를 암시 적으로 오프셋하는 것을 중지 position - (useHeader() ? 1 : 0)하고 대신 공개 메서드 int offsetPosition(int position)를 만들었 습니다. OnItemTouchListenerRecyclerview 를 설정하면 터치를 가로 채고, 터치의 x, y 좌표를 얻고, 해당 자식 뷰 를 찾은 다음 호출 recyclerView.getChildPosition(...)하면 항상 어댑터에서 오프셋되지 않은 위치를 얻을 수 있습니다! 이것은 RecyclerView 코드의 단점입니다.이 문제를 극복 할 수있는 쉬운 방법은 없습니다. 이것이 내가 필요로 할 때 내 자신의 코드 로 명시적인 위치를 오프셋하는 이유 입니다.


답변

나는 이것을 시도하지 않았지만 어댑터의 getItemCount가 반환하는 정수에 1 (또는 머리글과 바닥 글을 모두 원하는 경우 2)을 추가합니다. 그런 다음 getItemViewType어댑터에서 재정 의하여 다음과 같은 경우 다른 정수를 반환 할 수 있습니다 i==0. https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html#getItemViewType(int)

createViewHolder그런 다음에서 반환 한 정수를 전달 getItemViewType하여 헤더 뷰에 대해 뷰 홀더를 다르게 만들거나 구성 할 수 있습니다. https://developer.android.com/reference/android/support/v7/widget/RecyclerView.Adapter.html# createViewHolder (android.view.ViewGroup , int)

에 전달 된 위치 정수에서 1을 빼는 것을 잊지 마십시오 bindViewHolder.


답변

GitHub 라이브러리를 사용 하여 헤더 및 / 또는 바닥 글 을 추가 할 수 있습니다. 가능한 가장 간단한 방법으로 RecyclerView 에 있습니다.

프로젝트 에 HFRecyclerView 라이브러리 를 추가해야 하거나 Gradle에서 가져올 수도 있습니다.

compile 'com.mikhaellopez:hfrecyclerview:1.0.0'

이것은 이미지의 결과입니다.

시사

편집하다:

이 라이브러리를 사용하여 상단 및 / 또는 하단에 여백을 추가하려면 SimpleItemDecoration :

int offsetPx = 10;
recyclerView.addItemDecoration(new StartOffsetItemDecoration(offsetPx));
recyclerView.addItemDecoration(new EndOffsetItemDecoration(offsetPx));


답변

결국 다른 어댑터를 래핑하고 머리글 및 바닥 글보기를 추가하는 방법을 제공하기 위해 자체 어댑터를 구현했습니다.

여기에 요점 생성 : HeaderViewRecyclerAdapter.java

내가 원했던 주요 기능은 ListView와 유사한 인터페이스 였기 때문에 내 Fragment에서 뷰를 확장하고 .NET에 추가 할 수 있기를 원 RecyclerView했습니다 onCreateView. 이것은 작성하여 수행됩니다 HeaderViewRecyclerAdapter어댑터를 되풀이하는 전달 및 호출 addHeaderView하고 addFooterView당신의 팽창 의견을 전달합니다. 그런 다음 HeaderViewRecyclerAdapter인스턴스를 어댑터로 설정합니다 .RecyclerView .

추가 요구 사항은 머리글과 바닥 글을 유지하면서 어댑터를 쉽게 교체 할 수 있어야한다는 것이 었습니다. 이러한 머리글과 바닥 글의 여러 인스턴스가있는 여러 어댑터를 갖고 싶지 않았습니다. 따라서 setAdapter헤더와 바닥 글은 그대로두고 래핑 된 어댑터를 변경하기 위해 호출 할 수 있으며 변경 사항에 대한 RecyclerView알림을받을 수 있습니다.


답변

내 “간단하게 어리석게 유지”하는 방법 … 일부 리소스를 낭비하지만, 내 코드가 단순하게 유지되므로 신경 쓰지 않습니다. 1) 항목 _ 레이아웃에 가시성이있는 바닥 글 추가

  <LinearLayout
        android:id="@+id/footer"
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:orientation="vertical"
        android:visibility="gone">
    </LinearLayout>

2) 마지막 항목에 표시되도록 설정

public void onBindViewHolder(ChannelAdapter.MyViewHolder holder, int position) {
        boolean last = position==data.size()-1;
        //....
        holder.footer.setVisibility(View.GONE);
        if (last && showFooter){
            holder.footer.setVisibility(View.VISIBLE);
        }
    }

헤더에 대해 반대로 수행