如何在recyclerview中实现粘性页脚

时间:2015-11-24 10:07:09

标签: android android-recyclerview footer

我有RecyclerView,我需要下一步行为:

  • 如果有很多项目(更多适合屏幕) - 页脚是最后一项
  • 如果很少项目/没有项目 - 页脚位于屏幕底部

请告知我该如何实现此行为。

8 个答案:

答案 0 :(得分:10)

您可以使用RecyclerView.ItemDecoration来实现此行为。

public class StickyFooterItemDecoration extends RecyclerView.ItemDecoration {

    /**
     * Top offset to completely hide footer from the screen and therefore avoid noticeable blink during changing position of the footer.
     */
    private static final int OFF_SCREEN_OFFSET = 5000;

    @Override
    public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent, RecyclerView.State state) {
        int adapterItemCount = parent.getAdapter().getItemCount();
        if (isFooter(parent, view, adapterItemCount)) {
            //For the first time, each view doesn't contain any parameters related to its size,
            //hence we can't calculate the appropriate offset.
            //In this case, set a big top offset and notify adapter to update footer one more time.
            //Also, we shouldn't do it if footer became visible after scrolling.
            if (view.getHeight() == 0 && state.didStructureChange()) {
                hideFooterAndUpdate(outRect, view, parent);
            } else {
                outRect.set(0, calculateTopOffset(parent, view, adapterItemCount), 0, 0);
            }
        }
    }

    private void hideFooterAndUpdate(Rect outRect, final View footerView, final RecyclerView parent) {
        outRect.set(0, OFF_SCREEN_OFFSET, 0, 0);
        footerView.post(new Runnable() {
            @Override
            public void run() {
                parent.getAdapter().notifyDataSetChanged();
            }
        });
    }

    private int calculateTopOffset(RecyclerView parent, View footerView, int itemCount) {
        int topOffset = parent.getHeight() - visibleChildsHeightWithFooter(parent, footerView, itemCount);
        return topOffset < 0 ? 0 : topOffset;
    }

    private int visibleChildsHeightWithFooter(RecyclerView parent, View footerView, int itemCount) {
        int totalHeight = 0;
        //In the case of dynamic content when adding or removing are possible itemCount from the adapter is reliable,
        //but when the screen can fit fewer items than in adapter, getChildCount() from RecyclerView should be used.
        int onScreenItemCount = Math.min(parent.getChildCount(), itemCount);
        for (int i = 0; i < onScreenItemCount - 1; i++) {
            totalHeight += parent.getChildAt(i).getHeight();
        }
        return totalHeight + footerView.getHeight();
    }

    private boolean isFooter(RecyclerView parent, View view, int itemCount) {
        return parent.getChildAdapterPosition(view) == itemCount - 1;
    }
}

确保为RecyclerView高度设置 match_parent

请查看示例应用https://github.com/JohnKuper/recyclerview-sticky-footer及其工作原理http://sendvid.com/nbpj0806

此解决方案的一个巨大缺点是它只能在整个应用程序(不在装饰内)中的notifyDataSetChanged()之后正常工作。通过更具体的通知,它将无法正常工作并支持它们,它需要更多的逻辑。此外,您可以通过 eowise 从图书馆 recyclerview-stickyheaders 获取见解,并改进此解决方案。

答案 1 :(得分:6)

对Dmitriy Korobeynikov进行即兴创作并解决调用通知数据集的问题已更改

public class StickyFooterItemDecoration extends RecyclerView.ItemDecoration {

  @Override
  public void getItemOffsets(Rect outRect, final View view, final RecyclerView parent,
      RecyclerView.State state) {

    int position = parent.getChildAdapterPosition(view);
    int adapterItemCount = parent.getAdapter().getItemCount();
    if (adapterItemCount == RecyclerView.NO_POSITION || (adapterItemCount - 1) != position) {
      return;
    }
    outRect.top = calculateTopOffset(parent, view, adapterItemCount);
  }


  private int calculateTopOffset(RecyclerView parent, View footerView, int itemCount) {
    int topOffset =
        parent.getHeight() - parent.getPaddingTop() - parent.getPaddingBottom()
            - visibleChildHeightWithFooter(parent, footerView, itemCount);
    return topOffset < 0 ? 0 : topOffset;
  }



  private int visibleChildHeightWithFooter(RecyclerView parent, View footerView, int itemCount) {
    int totalHeight = 0;
    int onScreenItemCount = Math.min(parent.getChildCount(), itemCount);
    for (int i = 0; i < onScreenItemCount - 1; i++) {
      RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) parent.getChildAt(i)
          .getLayoutParams();
      int height =
          parent.getChildAt(i).getHeight() + layoutParams.topMargin
              + layoutParams.bottomMargin;
      totalHeight += height;
    }
    int footerHeight = footerView.getHeight();
    if (footerHeight == 0) {
      fixLayoutSize(footerView, parent);
      footerHeight = footerView.getHeight();
    }
    footerHeight = footerHeight + footerView.getPaddingBottom() + footerView.getPaddingTop();

    return totalHeight + footerHeight;
  }

  private void fixLayoutSize(View view, ViewGroup parent) {
    // Check if the view has a layout parameter and if it does not create one for it
    if (view.getLayoutParams() == null) {
      view.setLayoutParams(new ViewGroup.LayoutParams(
          ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
    }

    // Create a width and height spec using the parent as an example:
    // For width we make sure that the item matches exactly what it measures from the parent.
    //  IE if layout says to match_parent it will be exactly parent.getWidth()
    int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
    // For the height we are going to create a spec that says it doesn't really care what is calculated,
    //  even if its larger than the screen
    int heightSpec = View.MeasureSpec
        .makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);

    // Get the child specs using the parent spec and the padding the parent has
    int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
        parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
    int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
        parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);

    // Finally we measure the sizes with the actual view which does margin and padding changes to the sizes calculated
    view.measure(childWidth, childHeight);

    // And now we setup the layout for the view to ensure it has the correct sizes.
    view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
  }
}

答案 2 :(得分:1)

我正在使用带有权重的Linearlayout。我为页脚重量创建了多个值,它完美地运行。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="vertical"

<include layout="@layout/header" />

    <android.support.v7.widget.RecyclerView
    android:id="@+id/recycleView"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="0.5"
    tools:layout_height="0dp"
    tools:listitem="@layout/row" />

<TextView
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="@dimen/footer_weight"
    android:padding="@dimen/extra_padding"
    android:paddingEnd="@dimen/small_padding"
    android:paddingLeft="@dimen/small_padding"
    android:paddingRight="@dimen/small_padding"
    android:paddingStart="@dimen/small_padding"
    android:text="@string/contact"
    android:textColor="@color/grey" />

 </LinearLayout>

答案 3 :(得分:1)

所有这些解决方案都不起作用。当您最小化应用程序并再次打开它时,页脚会比屏幕底部低,并且您需要滚动查看它,即使只有1-2项。 您可以在xml中的回收站视图下添加页脚视图。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white">

<android.support.v4.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:overScrollMode="never"
    android:scrollbars="none">

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

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <android.support.v4.widget.Space
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:minHeight="1dp" />

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <include layout="@layout/recyclerView_footer" />
        </FrameLayout>
    </LinearLayout>
</android.support.v4.widget.NestedScrollView>

注意 - 我使用NestedScrollView和

    recyclerView.isNestedScrollingEnabled = false

SpaceView has weight 1 and height = 0dp以及linear layoutNestedScrollView has height = match_parent内的所有内容,现在我将页脚粘贴在底部,当列表变大时它会进一步移动

答案 4 :(得分:0)

我知道,这是一个老问题,但我会为那些将来会寻求此类决定的人添加一个答案。 可能将最后一项保留在屏幕底部,以防您只有很少或没有项目,并且当您有许多项目时,使用recyclerview滚动最后一项。

如何实现。您的RecyclerView适配器应该应用多种视图类型:视图,应显示为列表项;视图,应显示为页脚;一个空的视图。您可以在此处查看如何将具有不同视图的项目放入RecyclerView:https://stackoverflow.com/a/29362643/6329995 在主列表和页脚视图之间找到一个空视图。 然后在onBindViewHolder中为空视图检查您的主列表视图和页脚视图是否占用所有屏幕。如果是 - 将空视图高度设置为零,否则将其设置为看起来不被项目和页脚占用的高度。就这样。 删除/添加行时,您还可以动态更新该高度。更新列表后,只需为空白空间项调用notifyItemChanged。

您还要将您的RecyclerView高度设置为match_parent或精确高度,而不是wrap_content!

希望这有帮助。

答案 5 :(得分:0)

class FooterViewHolder(private val parent: ViewGroup) {

...

fun bind(item: Item) {
    ...
    itemView.post(::adjustTop)
}

private fun adjustTop() {
    val parent = parent as RecyclerView
    var topOffset = parent.height
    for (child in parent.children) topOffset -= child.height
    (itemView.layoutParams as ViewGroup.MarginLayoutParams)
        .setMargins(0, topOffset.coerceAtLeast(0), 0, 0)
}
}

答案 6 :(得分:0)

所选答案有缺陷。我已经对此发表评论并解释了为什么。如果您有兴趣,您可能需要阅读。

因此,如果选择的答案是错误的,还有什么其他更好的方法来解决这个问题?

1)像这样创建布局:

<ConsraintLayout>
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipToPadding="false"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <-- This is your footer and it can be anything you want -->
    <TextView
        android:id="@+id/yourFooter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</ConstraintLayout>

2)将页脚的高度设置为RecyclerView的bottomPadding。在preDraw上进行操作非常重要,这样您才能拥有合适的身高或脚注尺寸。

view.doOnPreDraw {
    val footerheight = yourFooter.height
    recyclerView.updatePadding(bottom = footerHeight)
    ...
}

3)现在,您需要做的就是聆听recyclerview滚动并聆听何时需要在正确的时间转换页脚。因此,请执行以下操作:

view.doOnPreDraw {
    val footerheight = yourFooter.height
    recyclerView.updatePadding(bottom = footerHeight)

    recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            val range = recyclerView.computeVerticalScrollRange()
            val extent = recyclerView.computeVerticalScrollExtent()
            val offset = recyclerView.computeVerticalScrollOffset()
            val threshHold = range - footerHeight
            val currentScroll = extent + offset
            val excess = currentScroll - threshHold
            yourFooter.transalationX = if (excess > 0)
                footerHeight * (excess.toFloat()/footerHeight.toFloat()) else 0F
        }
    })
}

希望这对将来的人很有帮助。

答案 7 :(得分:-1)

如果您不能忘记RecyclerView并使用ListView,那么请查看此链接Is there an addHeaderView equivalent for RecyclerView?它拥有您需要的一切。这是关于标题,但它几乎是相同的,除了标题位于列表的开头,页脚最后。