【问题标题】:Android 5.0 - Add header/footer to a RecyclerViewAndroid 5.0 - 向 RecyclerView 添加页眉/页脚
【发布时间】:2014-12-14 10:27:45
【问题描述】:

我花了一点时间试图找出一种将标题添加到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) 方法并将我的标题视图添加到第一个位置以充当标题。

这就是事情变得更丑陋的地方:

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 尝试在 Activity 创建的不同时刻调用addView(View view) 之后(也尝试在所有设置完成后添加视图,甚至是适配器的数据),我意识到我不知道这是不是正确的方法(而且看起来并不)。

PS:另外,非常感谢除了LinearLayoutManager 之外还可以处理GridLayoutManager 的解决方案!

【问题讨论】:

标签: java android android-recyclerview android-5.0-lollipop


【解决方案1】:

我必须在RecyclerView 中添加页脚,在这里我分享我的代码 sn-p,因为我认为它可能有用。请检查代码中的 cmets,以便更好地了解整体流程。

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
        }
    }
}

上面的代码 sn -p 为RecyclerView 添加了一个页脚。您可以检查this GitHub repository 以检查添加页眉和页脚的实现。

【讨论】:

  • 效果很好。这应该被标记为正确答案。
  • 这正是我所做的。但是如果我想让我的 RecyclerView 适应交错列表呢?第一个元素(标题)也将交错。 :(
  • 这是一个关于如何动态填充RecyclerView 的教程。您可以控制RecyclerView 的每个元素。请查看工作项目的代码部分。希望它可能会有所帮助。 github.com/comeondude/dynamic-recyclerview/wiki
  • int getItemViewType (int position) - 返回位置项的视图类型,以便视图回收。此方法的默认实现返回 0,假设适配器具有单一视图类型。与ListView 适配器不同,类型不必是连续的。考虑使用 id 资源来唯一标识项目视图类型。 - 从文档。 developer.android.com/reference/android/support/v7/widget/…
  • 对于人们一直想做的事情,需要做大量的手工工作。我简直不敢相信……
【解决方案2】:

很容易解决!!

我不喜欢将适配器内部的逻辑作为不同的视图类型的想法,因为每次它在返回视图之前都会检查视图类型。下面的解决方案避免了额外的检查。

只需在 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 将尝试布置所有视图持有者,而不管 RV 的layout_height

推荐用于导航抽屉或设置等小尺寸列表。

【讨论】:

  • 为我工作相当简单
  • 当页眉和页脚不必在列表中重复时,这是一种将页眉和页脚添加到回收站视图的非常简单的方法。
  • 这是失去RecyclerView带来的所有优势的一种非常简单的方法——你失去了实际的回收和它带来的优化。
  • 我试过这段代码,滚动不能正常工作......它变得滚动太慢了..请建议是否可以为此做点什么
  • 使用嵌套的scrollview会导致recyclerview缓存所有的view,如果recycler view的大小更多的滚动和加载时间会增加。我建议不要使用此代码
【解决方案3】:

我在 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)。因为如果你在Recyclerview上设置了OnItemTouchListener,就可以截取触摸,获取触摸的x,y坐标,找到对应的子视图然后调用recyclerView.getChildPosition(...),你总会得到适配器中的非偏移位置!这是 RecyclerView 代码中的一个缺点,我没有看到一个简单的方法来克服这个问题。这就是为什么我现在需要通过我的自己的代码来显式偏移位置。

【讨论】:

  • 看起来不错!有什么问题吗?或者我们可以安全地使用它? :D
  • @OvidiuLatcu 查看帖子
  • 在这些实现中,您似乎假设页眉和页脚的数量各只有 1 个?
  • @seb Version 2 就像魅力一样!我唯一需要修改的是在 onBindViewHolder 和 getItemViewType 方法上获取页脚的条件。问题是,如果您使用 position == getBasicItemCount() 获得位置,它不会为实际的最后一个位置返回 true,而是最后一个位置 - 1。它最终将 FooterView 放在那里(而不是在底部)。我们修复了它,将条件更改为 position == getBasicItemCount() + 1,效果很好!
  • @seb 第 2 版效果很好。非常感谢。我正在使用它。我建议的一件小事是为覆盖函数添加“final”关键字。
【解决方案4】:

我没有尝试过,但我会简单地将 1(或 2,如果您需要页眉和页脚)添加到适配器中 getItemCount 返回的整数。然后,您可以在适配器中覆盖 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)

不要忘记从传递给bindViewHolder 的位置整数中减一。

【讨论】:

  • 我认为这不是 recyclerview 的工作。 Recyclerviews 的工作是简单地回收视图。编写带有页眉或页脚实现的布局管理器是可行的方法。
  • 感谢@IanNewson 的回复。首先,即使只使用getItemViewType(int position) { return position == 0 ? 0 : 1; }RecyclerView 没有getViewTypeCount() 方法),这个解决方案似乎也有效。另一方面,我同意@IZI_Shadow_IZI,我真的觉得 LayoutManager 应该是处理这类事情的人。还有其他想法吗?
  • @VieuMa 你可能都是对的,但我目前不知道该怎么做,我很确定我的解决方案会奏效。次优解决方案总比没有解决方案好,这是您以前的情况。
  • @VieuMa 同样,将页眉和页脚抽象到适配器中意味着它应该处理请求的两种类型的布局,编写自己的布局管理器意味着重新实现这两种类型的布局。
  • 只需创建一个包含任何适配器的适配器,并在索引 0 处添加对标题视图的支持。列表视图中的 HeaderView 创建了许多边缘情况,并且附加值很小,因为使用它很容易解决问题一个包装适配器。
【解决方案5】:

您可以使用这个 GitHub 库,允许在您的 RecyclerView 中添加 Header 和/或 Footer最简单的方法。

您需要在项目中添加HFRecyclerView 库,或者您也可以从 Gradle 中获取它:

compile 'com.mikhaellopez:hfrecyclerview:1.0.0'

这是图像中的结果:

编辑:

如果您只想在此库的顶部和/或底部添加边距:SimpleItemDecoration:

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

【讨论】:

  • 这个库在 LinearLayoutManager 的标题上正确添加了视图,但我想在 GridLayoutManager 中将视图设置为标题,这发生在屏幕的整个宽度上。这个库可以吗?
  • 不,这个库允许您在适应时更改 recyclerView 的第一个和最后一个元素(RecyclerView.Adapter)。您可以毫无问题地将此适配器用于 GridView。所以我认为这个库可以做你想做的事。
【解决方案6】:

我最终实现了自己的适配器来包装任何其他适配器并提供添加页眉和页脚视图的方法。

在这里创建了一个要点:HeaderViewRecyclerAdapter.java

我想要的主要功能是类似于 ListView 的界面,因此我希望能够在 Fragment 中扩充视图并将它们添加到 onCreateView 中的 RecyclerView。这是通过创建一个HeaderViewRecyclerAdapter 传递要包装的适配器,并调用addHeaderViewaddFooterView 传递你的膨胀视图来完成的。然后将HeaderViewRecyclerAdapter实例设置为RecyclerView上的适配器。

一个额外的要求是我需要能够在保留页眉和页脚的同时轻松换出适配器,我不希望多个适配器具有这些页眉和页脚的多个实例。因此,您可以调用 setAdapter 更改包装的适配器,使页眉和页脚保持不变,并通知 RecyclerView 更改。

【讨论】:

    【解决方案7】:

    recyclerview:1.2.0 引入了ConcatAdapter 类,它将多个适配器连接成一个。因此它允许创建单独的页眉/页脚适配器并在多个列表中重用它们。

    myRecyclerView.adapter = ConcatAdapter(headerAdapter, listAdapter, footerAdapter)
    

    看看announcement article。它包含一个示例如何使用ConcatAdapter 在页眉和页脚中显示加载进度。

    在我发布此答案的那一刻,库的版本 1.2.0 处于 alpha 阶段,因此 api 可能会更改。您可以查看状态here

    【讨论】:

    • 这个解决方案有个问题,如果你使用 ListAdapter submitList 方法,它会一直向下滚动到底部。不推荐。
    【解决方案8】:

    我的“保持简单愚蠢”的方式......它浪费了一些资源,我知道,但我不在乎,因为我的代码保持简单 所以... 首先,将可见性 GONE 的页脚添加到您的 item_layout

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

    然后,将其设置为在最后一项上可见

    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);
            }
        }
    

    对标题做相反的事情

    【讨论】:

    • 除了这个解决方案浪费资源外,我还有一个功能问题。当使用 ListAdapter 和 submitList() 方法的数据列表少一项时,并且少一项是显示页脚的最后一项,页脚永远不会显示。我认为这是因为 itemCount(与 DiffUtil 结合使用)保持不变。谨防。我认为最好的解决方案是这个medium.com/androiddevelopers/…
    【解决方案9】:

    基于@seb 的解决方案,我创建了一个RecyclerView.Adapter 的子类,它支持任意数量的页眉和页脚。

    https://gist.github.com/mheras/0908873267def75dc746

    虽然看起来是个解决办法,但我也觉得这东西应该由LayoutManager来管理。不幸的是,我现在需要它,而且我没有时间从头开始实现 StaggeredGridLayoutManager(甚至从它扩展)。

    我仍在测试它,但如果您愿意,可以尝试一下。如果您发现任何问题,请告诉我。

    【讨论】:

      【解决方案10】:

      你可以使用 viewtype 来解决这个问题,这是我的演示: https://github.com/yefengfreedom/RecyclerViewWithHeaderFooterLoadingEmptyViewErrorView

      1. 你可以定义一些recycler view显示模式:

        public static final int MODE_DATA = 0, MODE_LOADING = 1, MODE_ERROR = 2, MODE_EMPTY = 3, MODE_HEADER_VIEW = 4, MODE_FOOTER_VIEW = 5;

      2.覆盖getItemViewType方法

       @Override
      public int getItemViewType(int position) {
          if (mMode == RecyclerViewMode.MODE_LOADING) {
              return RecyclerViewMode.MODE_LOADING;
          }
          if (mMode == RecyclerViewMode.MODE_ERROR) {
              return RecyclerViewMode.MODE_ERROR;
          }
          if (mMode == RecyclerViewMode.MODE_EMPTY) {
              return RecyclerViewMode.MODE_EMPTY;
          }
          //check what type our position is, based on the assumption that the order is headers > items > footers
          if (position < mHeaders.size()) {
              return RecyclerViewMode.MODE_HEADER_VIEW;
          } else if (position >= mHeaders.size() + mData.size()) {
              return RecyclerViewMode.MODE_FOOTER_VIEW;
          }
          return RecyclerViewMode.MODE_DATA;
      }
      

      3.覆盖getItemCount方法

      @Override
      public int getItemCount() {
          if (mMode == RecyclerViewMode.MODE_DATA) {
              return mData.size() + mHeaders.size() + mFooters.size();
          } else {
              return 1;
          }
      }
      

      4.覆盖 onCreateViewHolder 方法。通过 viewType 创建视图持有者

      @Override
      public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
          if (viewType == RecyclerViewMode.MODE_LOADING) {
              RecyclerView.ViewHolder loadingViewHolder = onCreateLoadingViewHolder(parent);
              loadingViewHolder.itemView.setLayoutParams(
                      new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
              );
              return loadingViewHolder;
          }
          if (viewType == RecyclerViewMode.MODE_ERROR) {
              RecyclerView.ViewHolder errorViewHolder = onCreateErrorViewHolder(parent);
              errorViewHolder.itemView.setLayoutParams(
                      new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
              );
              errorViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(final View v) {
                      if (null != mOnErrorViewClickListener) {
                          new Handler().postDelayed(new Runnable() {
                              @Override
                              public void run() {
                                  mOnErrorViewClickListener.onErrorViewClick(v);
                              }
                          }, 200);
                      }
                  }
              });
              return errorViewHolder;
          }
          if (viewType == RecyclerViewMode.MODE_EMPTY) {
              RecyclerView.ViewHolder emptyViewHolder = onCreateEmptyViewHolder(parent);
              emptyViewHolder.itemView.setLayoutParams(
                      new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, parent.getHeight() - mToolBarHeight)
              );
              emptyViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(final View v) {
                      if (null != mOnEmptyViewClickListener) {
                          new Handler().postDelayed(new Runnable() {
                              @Override
                              public void run() {
                                  mOnEmptyViewClickListener.onEmptyViewClick(v);
                              }
                          }, 200);
                      }
                  }
              });
              return emptyViewHolder;
          }
          if (viewType == RecyclerViewMode.MODE_HEADER_VIEW) {
              RecyclerView.ViewHolder headerViewHolder = onCreateHeaderViewHolder(parent);
              headerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(final View v) {
                      if (null != mOnHeaderViewClickListener) {
                          new Handler().postDelayed(new Runnable() {
                              @Override
                              public void run() {
                                  mOnHeaderViewClickListener.onHeaderViewClick(v, v.getTag());
                              }
                          }, 200);
                      }
                  }
              });
              return headerViewHolder;
          }
          if (viewType == RecyclerViewMode.MODE_FOOTER_VIEW) {
              RecyclerView.ViewHolder footerViewHolder = onCreateFooterViewHolder(parent);
              footerViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
                  @Override
                  public void onClick(final View v) {
                      if (null != mOnFooterViewClickListener) {
                          new Handler().postDelayed(new Runnable() {
                              @Override
                              public void run() {
                                  mOnFooterViewClickListener.onFooterViewClick(v, v.getTag());
                              }
                          }, 200);
                      }
                  }
              });
              return footerViewHolder;
          }
          RecyclerView.ViewHolder dataViewHolder = onCreateDataViewHolder(parent);
          dataViewHolder.itemView.setOnClickListener(new View.OnClickListener() {
              @Override
              public void onClick(final View v) {
                  if (null != mOnItemClickListener) {
                      new Handler().postDelayed(new Runnable() {
                          @Override
                          public void run() {
                              mOnItemClickListener.onItemClick(v, v.getTag());
                          }
                      }, 200);
                  }
              }
          });
          dataViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
              @Override
              public boolean onLongClick(final View v) {
                  if (null != mOnItemLongClickListener) {
                      new Handler().postDelayed(new Runnable() {
                          @Override
                          public void run() {
                              mOnItemLongClickListener.onItemLongClick(v, v.getTag());
                          }
                      }, 200);
                      return true;
                  }
                  return false;
              }
          });
          return dataViewHolder;
      }
      

      5.重写 onBindViewHolder 方法。按viewType绑定数据

      @Override
      public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
          if (mMode == RecyclerViewMode.MODE_LOADING) {
              onBindLoadingViewHolder(holder, position);
          } else if (mMode == RecyclerViewMode.MODE_ERROR) {
              onBindErrorViewHolder(holder, position);
          } else if (mMode == RecyclerViewMode.MODE_EMPTY) {
              onBindEmptyViewHolder(holder, position);
          } else {
              if (position < mHeaders.size()) {
                  if (mHeaders.size() > 0) {
                      onBindHeaderViewHolder(holder, position);
                  }
              } else if (position >= mHeaders.size() + mData.size()) {
                  if (mFooters.size() > 0) {
                      onBindFooterViewHolder(holder, position - mHeaders.size() - mData.size());
                  }
              } else {
                  onBindDataViewHolder(holder, position - mHeaders.size());
              }
          }
      }
      

      【讨论】:

      • 如果您的链接将来会损坏怎么办?
      • 好问题。我将编辑我的答案并在此处发布我的代码
      【解决方案11】:

      您可以使用库 SectionedRecyclerViewAdapter 将您的项目分组并为每个部分添加标题,如下图所示:

      首先你创建你的部分类:

      class MySection extends StatelessSection {
      
          String title;
          List<String> list;
      
          public MySection(String title, List<String> list) {
              // call constructor with layout resources for this Section header, footer and items 
              super(R.layout.section_header, R.layout.section_item);
      
              this.title = title;
              this.list = list;
          }
      
          @Override
          public int getContentItemsTotal() {
              return list.size(); // number of items of this section
          }
      
          @Override
          public RecyclerView.ViewHolder getItemViewHolder(View view) {
              // return a custom instance of ViewHolder for the items of this section
              return new MyItemViewHolder(view);
          }
      
          @Override
          public void onBindItemViewHolder(RecyclerView.ViewHolder holder, int position) {
              MyItemViewHolder itemHolder = (MyItemViewHolder) holder;
      
              // bind your view here
              itemHolder.tvItem.setText(list.get(position));
          }
      
          @Override
          public RecyclerView.ViewHolder getHeaderViewHolder(View view) {
              return new SimpleHeaderViewHolder(view);
          }
      
          @Override
          public void onBindHeaderViewHolder(RecyclerView.ViewHolder holder) {
              MyHeaderViewHolder headerHolder = (MyHeaderViewHolder) holder;
      
              // bind your header view here
              headerHolder.tvItem.setText(title);
          }
      }
      

      然后您使用您的部分设置 RecyclerView 并使用 GridLayoutManager 更改标题的 SpanSize:

      // Create an instance of SectionedRecyclerViewAdapter 
      SectionedRecyclerViewAdapter sectionAdapter = new SectionedRecyclerViewAdapter();
      
      // Create your sections with the list of data
      MySection section1 = new MySection("My Section 1 title", dataList1);
      MySection section2 = new MySection("My Section 2 title", dataList2);
      
      // Add your Sections to the adapter
      sectionAdapter.addSection(section1);
      sectionAdapter.addSection(section2);
      
      // Set up a GridLayoutManager to change the SpanSize of the header
      GridLayoutManager glm = new GridLayoutManager(getContext(), 2);
      glm.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
          @Override
          public int getSpanSize(int position) {
              switch(sectionAdapter.getSectionItemViewType(position)) {
                  case SectionedRecyclerViewAdapter.VIEW_TYPE_HEADER:
                      return 2;
                  default:
                      return 1;
              }
          }
      });
      
      // Set up your RecyclerView with the SectionedRecyclerViewAdapter
      RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerview);
      recyclerView.setLayoutManager(glm);
      recyclerView.setAdapter(sectionAdapter);
      

      【讨论】:

        【解决方案12】:

        我只想为所有这些 HeaderRecyclerViewAdapter 实现添加一个替代方案。复合适配器:

        https://github.com/negusoft/CompoundAdapter-android

        这是一种更灵活的方法,因为您可以从 Adapters 中创建一个 AdapterGroup。对于标题示例,请按原样使用您的适配器,以及包含标题一项的适配器:

        AdapterGroup adapterGroup = new AdapterGroup();
        adapterGroup.addAdapter(SingleAdapter.create(R.layout.header));
        adapterGroup.addAdapter(new MyAdapter(...));
        
        recyclerView.setAdapter(adapterGroup);
        

        它相当简单易读。您可以使用相同的原理轻松实现更复杂的适配器。

        【讨论】:

        • 这是我找到的最简单最有力的答案
        【解决方案13】:

        @reaz-murshed 的出色回答在这里分享。但我不喜欢在数据大小中添加 +1 并在到达末尾时返回页脚视图的部分。
        它告诉每个最后一个元素都是一个页脚视图,我很难删除页脚视图。

        而是为我的情况做了类似的事情 -

        private List<RealResponse> addEmptyLoaderResponse(List<RealResponse> originalList){
            if(originalList == null){
                originalList= new ArrayList<>();
            }
            originalList.add(new EmptyRealResponse());
            return originalList;
        }
        private class EmptyRealResponse extends RealResponse{
            /**Just an Empty class as placeholder for loader at Footer View
             *
             */
        }
        public void setItems(List<InconcurPostResponse> items) {
            this.items = addEmptyLoaderResponse(items);
        }
        @Override
        public int getItemCount() {
            return items.size();
        }
        
        @Override
        public int getItemViewType(int position){
            if(this.items.get(position) instanceof  EmptyRealResponse){
                return ViewTypes.FOOTER_VIEW_TYPE.getViewType();
            }
            return super.getItemViewType(position);
        }
        

        这对我来说更干净,它将实际对象加载到回收器视图中。另外,当我不需要它或者如果我想添加更多占位符页脚视图时,我确实得到了删除页脚视图的好处。

        【讨论】:

          【解决方案14】:

          我知道我来晚了,但直到最近我才能够为适配器实现这样的“addHeader”。在我的 FlexibleAdapter 项目中,您可以在 Sectionable 项目上调用 setHeader,然后调用 showAllHeaders。如果您只需要一个标题,那么第一项应该有标题。如果您删除此项,则标题会自动链接到下一项。

          不幸的是,页脚没有被覆盖(还)。

          FlexibleAdapter 允许您做的不仅仅是创建标题/部分。 你真的应该看看:https://github.com/davideas/FlexibleAdapter

          【讨论】:

            猜你喜欢
            • 2018-08-16
            • 1970-01-01
            • 2015-05-13
            • 2014-08-17
            • 2020-11-30
            • 1970-01-01
            • 1970-01-01
            • 2010-12-03
            相关资源
            最近更新 更多