如何在 Android 中制作粘性部分标题(如 iOS)?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20713046/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-20 03:19:57  来源:igfitidea点击:

How to make sticky section headers (like iOS) in Android?

androidioslistviewscrollview

提问by Daniel Gonzáles

My specific question is: How I can achieve an effect like this: http://youtu.be/EJm7subFbQI

我的具体问题是:如何实现这样的效果:http: //youtu.be/EJm7subFbQI

The bounce effect is not important, but i need the "sticky" effect for the headers. Where do I start?, In what can I base me? I need something that I can implement on API 8 to up.

反弹效果并不重要,但我需要标题的“粘性”效果。我从哪里开始?,我可以以什么为基础?我需要一些可以在 API 8 上实现的东西。

Thanks.

谢谢。

回答by Kyle Clegg

There are a few solutions that already exist for this problem. What you're describing are section headers and have come to be referred to as stickysection headers in Android.

对于这个问题,已经存在一些解决方案。您所描述的是部分标题,并且在 Android 中被称为粘性部分标题。

回答by Dennis K

EDIT: Had some free time to add the code of fully working example. Edited the answer accordingly.

编辑:有一些空闲时间来添加完整工作示例的代码。相应地编辑了答案。

For those who don't want to use 3rd party code (or cannot use it directly, e.g. in Xamarin), this could be done fairly easily by hand. The idea is to use another ListView for the header. This list view contains only the header items. It will not be scrollable by the user (setEnabled(false)), but will be scrolled from code based on main lists' scrolling. So you will have two lists - headerListview and mainListview, and two corresponding adapters headerAdapter and mainAdapter. headerAdapter only returns section views, while mainAdapter supports two view types (section and item). You will need a method that takes a position in the main list and returns a corresponding position in the sections list.

对于那些不想使用 3rd 方代码(或不能直接使用它,例如在 Xamarin 中)的人来说,这可以通过手工很容易地完成。这个想法是使用另一个 ListView 作为标题。此列表视图仅包含标题项。它不会被用户滚动 (setEnabled(false)),但会根据主列表的滚动从代码中滚动。因此,您将有两个列表 - headerListview 和 mainListview,以及两个相应的适配器 headerAdapter 和 mainAdapter。headerAdapter 只返回截面视图,而 mainAdapter 支持两种视图类型(section 和 item)。您将需要一个在主列表中获取位置并在部分列表中返回相应位置的方法。

Main activity

主要活动

public class MainActivity extends AppCompatActivity {

    public static final int TYPE_SECTION = 0;
    public static final int TYPE_ITEM = 1;

    ListView mainListView;
    ListView headerListView;
    MainAdapter mainAdapter;
    HeaderAdapter headerAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mainListView = (ListView)findViewById(R.id.list);
        headerListView = (ListView)findViewById(R.id.header);
        mainAdapter = new MainAdapter();
        headerAdapter = new HeaderAdapter();

        headerListView.setEnabled(false);
        headerListView.setAdapter(headerAdapter);
        mainListView.setAdapter(mainAdapter);

        mainListView.setOnScrollListener(new AbsListView.OnScrollListener(){

            @Override
            public void onScrollStateChanged(AbsListView view, int scrollState){

            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                // this should return an index in the headers list, based one the index in the main list. The logic for this is highly dependent on your data.
                int pos = mainAdapter.getSectionIndexForPosition(firstVisibleItem);
                // this makes sure our headerListview shows the proper section (the one on the top of the mainListview)
                headerListView.setSelection(pos);

                // this makes sure that headerListview is scrolled exactly the same amount as the mainListview
                if(mainAdapter.getItemViewType(firstVisibleItem + 1) == TYPE_SECTION){
                    headerListView.setSelectionFromTop(pos, mainListView.getChildAt(0).getTop());
                }
            }
        });
    }

    public class MainAdapter extends BaseAdapter{
        int count = 30;

        @Override
        public int getItemViewType(int position){
            if((float)position / 10 == (int)((float)position/10)){
                return TYPE_SECTION;
            }else{
                return TYPE_ITEM;
            }
        }

        @Override
        public int getViewTypeCount(){ return 2; }

        @Override
        public int getCount() { return count - 1; }

        @Override
        public Object getItem(int position) { return null; }

        @Override
        public long getItemId(int position) { return position; }

        public int getSectionIndexForPosition(int position){ return position / 10; }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v =  getLayoutInflater().inflate(R.layout.item, parent, false);
            position++;
            if(getItemViewType(position) == TYPE_SECTION){
                ((TextView)v.findViewById(R.id.text)).setText("SECTION "+position);

            }else{
                ((TextView)v.findViewById(R.id.text)).setText("Item "+position);
            }
            return v;
        }
    }

    public class HeaderAdapter extends BaseAdapter{
        int count = 5;

        @Override
        public int getCount() { return count; }

        @Override
        public Object getItem(int position) { return null; }

        @Override
        public long getItemId(int position) { return position; }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View v =  getLayoutInflater().inflate(R.layout.item, parent, false);
            ((TextView)v.findViewById(R.id.text)).setText("SECTION "+position*10);
            return v;
        }
    }

}

A couple of things to note here. We do not want to show the very first section in the main view list, because it would produce a duplicate (it's already shown in the header). To avoid that, in your mainAdapter.getCount():

这里有几点需要注意。我们不想在主视图列表中显示第一部分,因为它会产生重复(它已经显示在标题中)。为了避免这种情况,在您的 mainAdapter.getCount() 中:

return actualCount - 1;

and make sure the first line in your getView() method is

并确保 getView() 方法中的第一行是

position++;

This way your main list will be rendering all cells but the first one.

这样您的主列表将呈现除第一个单元格之外的所有单元格。

Another thing is that you want to make sure your headerListview's height matches the height of the list item. In this example the height is fixed, but it could be tricky if your items height is not set to an exact value in dp. Please refer to this answer for how to address this: https://stackoverflow.com/a/41577017/291688

另一件事是您要确保 headerListview 的高度与列表项的高度匹配。在此示例中,高度是固定的,但如果您的项目高度未设置为 dp 中的精确值,则可能会很棘手。请参阅此答案以了解如何解决此问题:https: //stackoverflow.com/a/41577017/291688

Main layout

主要布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">
    <ListView
        android:id="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="48dp"/>

    <ListView
        android:id="@+id/list"
        android:layout_below="@+id/header"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</RelativeLayout>

Item / header layout

项目/标题布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="48dp">
    <TextView
        android:id="@+id/text"
        android:gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        />

</LinearLayout>

回答by Prakhar1001

Add this in your app.gradle file

将此添加到您的 app.gradle 文件中

compile 'se.emilsjolander:StickyScrollViewItems:1.1.0'

then my layout, where I have added android:tag ="sticky"to specific views like textview or edittext not LinearLayout, looks like this. It also uses databinding, ignore that.

然后我的布局,我已经添加android:tag ="sticky"到特定的视图,如 textview 或 edittext 而不是 LinearLayout,看起来像这样。它还使用数据绑定,忽略它。

    <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="temp"
            type="com.lendingkart.prakhar.lendingkartdemo.databindingmodel.BusinessDetailFragmentModel" />

        <variable
            name="presenter"
            type="com.lendingkart.prakhar.lendingkartdemo.presenters.BusinessDetailsPresenter" />
    </data>

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


        <com.lendingkart.prakhar.lendingkartdemo.customview.StickyScrollView
            android:id="@+id/sticky_scroll"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <!-- scroll view child goes here -->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">


                <android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    card_view:cardCornerRadius="5dp"
                    card_view:cardUseCompatPadding="true">

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


                        <TextView
                            style="@style/group_view_text"
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:background="@drawable/businessdetailtitletextviewbackground"
                            android:padding="@dimen/activity_horizontal_margin"
                            android:tag="sticky"
                            android:text="@string/business_contact_detail" />

                        <android.support.design.widget.TextInputLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_margin="7dp">

                            <android.support.design.widget.TextInputEditText
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:hint="@string/comapnyLabel"
                                android:textSize="16sp" />

                        </android.support.design.widget.TextInputLayout>

                        <android.support.design.widget.TextInputLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_margin="5dp">

                            <android.support.design.widget.TextInputEditText
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:hint="@string/contactLabel"
                                android:textSize="16sp" />

                        </android.support.design.widget.TextInputLayout>

                        <android.support.design.widget.TextInputLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_margin="5dp">

                            <android.support.design.widget.TextInputEditText
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:hint="@string/emailLabel"
                                android:textSize="16sp" />

                        </android.support.design.widget.TextInputLayout>

                        <android.support.design.widget.TextInputLayout
                            android:layout_width="match_parent"
                            android:layout_height="wrap_content"
                            android:layout_margin="5dp">

                            <android.support.design.widget.TextInputEditText
                                android:layout_width="match_parent"
                                android:layout_height="wrap_content"
                                android:hint="@string/NumberOfEmployee"
                                android:textSize="16sp" />

                        </android.support.design.widget.TextInputLayout>


                    </LinearLayout>
                </android.support.v7.widget.CardView>

                <android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    card_view:cardCornerRadius="5dp"
                    card_view:cardUseCompatPadding="true">

                    <TextView
                        style="@style/group_view_text"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:background="@drawable/businessdetailtitletextviewbackground"
                        android:padding="@dimen/activity_horizontal_margin"
                        android:tag="sticky"
                        android:text="@string/nature_of_business" />


                </android.support.v7.widget.CardView>

                <android.support.v7.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_gravity="center"
                    card_view:cardCornerRadius="5dp"
                    card_view:cardUseCompatPadding="true">

                    <TextView
                        style="@style/group_view_text"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:background="@drawable/businessdetailtitletextviewbackground"
                        android:padding="@dimen/activity_horizontal_margin"
                        android:tag="sticky"
                        android:text="@string/taxation" />


                </android.support.v7.widget.CardView>



            </LinearLayout>
        </com.lendingkart.prakhar.lendingkartdemo.customview.StickyScrollView>


    </LinearLayout>
</layout>

style group for the textview looks this

文本视图的样式组看起来像这样

 <style name="group_view_text" parent="@android:style/TextAppearance.Medium">
        <item name="android:layout_width">wrap_content</item>
        <item name="android:layout_height">wrap_content</item>
        <item name="android:textColor">@color/edit_text_color</item>
        <item name="android:textSize">16dp</item>
        <item name="android:layout_centerVertical">true</item>
        <item name="android:textStyle">bold</item>
    </style>

and the background for the textview goes like this:(@drawable/businessdetailtitletextviewbackground)

和 textview 的背景是这样的:(@drawable/businessdetailtitletextviewbackground)

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <solid android:color="@color/edit_text_color" />
        </shape>
    </item>
    <item android:bottom="2dp">
        <shape android:shape="rectangle">
            <solid android:color="@color/White" />
        </shape>
    </item>
</layer-list>

回答by yoAlex5

You can reach this effect using SuperSLiMlibrary. It provides you a LayoutManager for RecyclerView with interchangeable linear, grid, and staggered displays of views.

您可以使用SuperSLiM库达到这种效果。它为 RecyclerView 提供了一个 LayoutManager,具有可互换的线性、网格和交错显示视图。

A good demo is located in github repository

一个好的demo位于github仓库

It is simply to get such result

就是为了得到这样的结果

app:slm_headerDisplay="inline|sticky"
or
app:slm_headerDisplay="sticky"

enter image description here

enter image description here

回答by Parsania Hardik

I have used one special class to achieve listview like iPhone. You can find example with source code here. https://demonuts.com/android-recyclerview-sticky-header-like-iphone/

我使用了一个特殊的类来实现像 iPhone 一样的列表视图。您可以在此处找到带有源代码的示例。https://demonuts.com/android-recyclerview-sticky-header-like-iphone/

This class which has updated listview is as

更新了列表视图的这个类是

import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType;
import android.widget.ListView;
import android.widget.RelativeLayout;

public class HeaderListView extends RelativeLayout {

    // TODO: Handle listViews with fast scroll
    // TODO: See if there are methods to dispatch to mListView

    private static final int FADE_DELAY    = 1000;
    private static final int FADE_DURATION = 2000;

    private InternalListView mListView;
    private SectionAdapter   mAdapter;
    private RelativeLayout   mHeader;
    private View             mHeaderConvertView;
    private FrameLayout      mScrollView;
    private AbsListView.OnScrollListener mExternalOnScrollListener;

    public HeaderListView(Context context) {
        super(context);
        init(context, null);
    }

    public HeaderListView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mListView = new InternalListView(getContext(), attrs);
        LayoutParams listParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        listParams.addRule(ALIGN_PARENT_TOP);
        mListView.setLayoutParams(listParams);
        mListView.setOnScrollListener(new HeaderListViewOnScrollListener());
        mListView.setVerticalScrollBarEnabled(false);
        mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                if (mAdapter != null) {
                    mAdapter.onItemClick(parent, view, position, id);
                }
            }
        });
        addView(mListView);

        mHeader = new RelativeLayout(getContext());
        LayoutParams headerParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
        headerParams.addRule(ALIGN_PARENT_TOP);
        mHeader.setLayoutParams(headerParams);
        mHeader.setGravity(Gravity.BOTTOM);
        addView(mHeader);

        // The list view's scroll bar can be hidden by the header, so we display our own scroll bar instead
        Drawable scrollBarDrawable = getResources().getDrawable(R.drawable.scrollbar_handle_holo_light);
        mScrollView = new FrameLayout(getContext());
        LayoutParams scrollParams = new LayoutParams(scrollBarDrawable.getIntrinsicWidth(), LayoutParams.MATCH_PARENT);
        scrollParams.addRule(ALIGN_PARENT_RIGHT);
        scrollParams.rightMargin = (int) dpToPx(2);
        mScrollView.setLayoutParams(scrollParams);

        ImageView scrollIndicator = new ImageView(context);
        scrollIndicator.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        scrollIndicator.setImageDrawable(scrollBarDrawable);
        scrollIndicator.setScaleType(ScaleType.FIT_XY);
        mScrollView.addView(scrollIndicator);
        mScrollView.setVisibility(INVISIBLE);

        addView(mScrollView);
    }

    public void setAdapter(SectionAdapter adapter) {
        mAdapter = adapter;
        mListView.setAdapter(adapter);
    }

    public void setOnScrollListener(AbsListView.OnScrollListener l) {
        mExternalOnScrollListener = l;
    }

    private class HeaderListViewOnScrollListener implements AbsListView.OnScrollListener {

        private int            previousFirstVisibleItem = -1;
        private int            direction                = 0;
        private int            actualSection            = 0;
        private boolean        scrollingStart           = false;
        private boolean        doneMeasuring            = false;
        private int            lastResetSection         = -1;
        private int            nextH;
        private int            prevH;
        private View           previous;
        private View           next;
        private AlphaAnimation fadeOut                  = new AlphaAnimation(1f, 0f);
        private boolean        noHeaderUpToHeader       = false;
        private boolean        didScroll = false;

        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if (mExternalOnScrollListener != null) {
                mExternalOnScrollListener.onScrollStateChanged(view, scrollState);
            }
            didScroll = true;
        }

        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            if (mExternalOnScrollListener != null) {
                mExternalOnScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount);
            }

            if (!didScroll) {
                return;
            }

            firstVisibleItem -= mListView.getHeaderViewsCount();
            if (firstVisibleItem < 0) {
                mHeader.removeAllViews();
                return;
            }

            updateScrollBar();
            if (visibleItemCount > 0 && firstVisibleItem == 0 && mHeader.getChildAt(0) == null) {
                addSectionHeader(0);
                lastResetSection = 0;
            }

            int realFirstVisibleItem = getRealFirstVisibleItem(firstVisibleItem, visibleItemCount);
            if (totalItemCount > 0 && previousFirstVisibleItem != realFirstVisibleItem) {
                direction = realFirstVisibleItem - previousFirstVisibleItem;

                actualSection = mAdapter.getSection(realFirstVisibleItem);

                boolean currIsHeader = mAdapter.isSectionHeader(realFirstVisibleItem);
                boolean prevHasHeader = mAdapter.hasSectionHeaderView(actualSection - 1);
                boolean nextHasHeader = mAdapter.hasSectionHeaderView(actualSection + 1);
                boolean currHasHeader = mAdapter.hasSectionHeaderView(actualSection);
                boolean currIsLast = mAdapter.getRowInSection(realFirstVisibleItem) == mAdapter.numberOfRows(actualSection) - 1;
                boolean prevHasRows = mAdapter.numberOfRows(actualSection - 1) > 0;
                boolean currIsFirst = mAdapter.getRowInSection(realFirstVisibleItem) == 0;

                boolean needScrolling = currIsFirst && !currHasHeader && prevHasHeader && realFirstVisibleItem != firstVisibleItem;
                boolean needNoHeaderUpToHeader = currIsLast && currHasHeader && !nextHasHeader && realFirstVisibleItem == firstVisibleItem && Math.abs(mListView.getChildAt(0).getTop()) >= mListView.getChildAt(0).getHeight() / 2;

                noHeaderUpToHeader = false;
                if (currIsHeader && !prevHasHeader && firstVisibleItem >= 0) {
                    resetHeader(direction < 0 ? actualSection - 1 : actualSection);
                } else if ((currIsHeader && firstVisibleItem > 0) || needScrolling) {
                    if (!prevHasRows) {
                        resetHeader(actualSection-1);
                    }
                    startScrolling();
                } else if (needNoHeaderUpToHeader) {
                    noHeaderUpToHeader = true;
                } else if (lastResetSection != actualSection) {
                    resetHeader(actualSection);
                }

                previousFirstVisibleItem = realFirstVisibleItem;
            }

            if (scrollingStart) {
                int scrolled = realFirstVisibleItem >= firstVisibleItem ? mListView.getChildAt(realFirstVisibleItem - firstVisibleItem).getTop() : 0;

                if (!doneMeasuring) {
                    setMeasurements(realFirstVisibleItem, firstVisibleItem);
                }

                int headerH = doneMeasuring ? (prevH - nextH) * direction * Math.abs(scrolled) / (direction < 0 ? nextH : prevH) + (direction > 0 ? nextH : prevH) : 0;

                mHeader.scrollTo(0, -Math.min(0, scrolled - headerH));
                if (doneMeasuring && headerH != mHeader.getLayoutParams().height) {
                    LayoutParams p = (LayoutParams) (direction < 0 ? next.getLayoutParams() : previous.getLayoutParams());
                    p.topMargin = headerH - p.height;
                    mHeader.getLayoutParams().height = headerH;
                    mHeader.requestLayout();
                }
            }

            if (noHeaderUpToHeader) {
                if (lastResetSection != actualSection) {
                    addSectionHeader(actualSection);
                    lastResetSection = actualSection + 1;
                }
                mHeader.scrollTo(0, mHeader.getLayoutParams().height - (mListView.getChildAt(0).getHeight() + mListView.getChildAt(0).getTop()));
            }
        }

        private void startScrolling() {
            scrollingStart = true;
            doneMeasuring = false;
            lastResetSection = -1;
        }

        private void resetHeader(int section) {
            scrollingStart = false;
            addSectionHeader(section);
            mHeader.requestLayout();
            lastResetSection = section;
        }

        private void setMeasurements(int realFirstVisibleItem, int firstVisibleItem) {

            if (direction > 0) {
                nextH = realFirstVisibleItem >= firstVisibleItem ? mListView.getChildAt(realFirstVisibleItem - firstVisibleItem).getMeasuredHeight() : 0;
            }

            previous = mHeader.getChildAt(0);
            prevH = previous != null ? previous.getMeasuredHeight() : mHeader.getHeight();

            if (direction < 0) {
                if (lastResetSection != actualSection - 1) {
                    addSectionHeader(Math.max(0, actualSection - 1));
                    next = mHeader.getChildAt(0);
                }
                nextH = mHeader.getChildCount() > 0 ? mHeader.getChildAt(0).getMeasuredHeight() : 0;
                mHeader.scrollTo(0, prevH);
            }
            doneMeasuring = previous != null && prevH > 0 && nextH > 0;
        }

        private void updateScrollBar() {
            if (mHeader != null && mListView != null && mScrollView != null) {
                int offset = mListView.computeVerticalScrollOffset();
                int range = mListView.computeVerticalScrollRange();
                int extent = mListView.computeVerticalScrollExtent();
                mScrollView.setVisibility(extent >= range ? View.INVISIBLE : View.VISIBLE);
                if (extent >= range) {
                    return;
                }
                int top = range == 0 ? mListView.getHeight() : mListView.getHeight() * offset / range;
                int bottom = range == 0 ? 0 : mListView.getHeight() - mListView.getHeight() * (offset + extent) / range;
                mScrollView.setPadding(0, top, 0, bottom);
                fadeOut.reset();
                fadeOut.setFillBefore(true);
                fadeOut.setFillAfter(true);
                fadeOut.setStartOffset(FADE_DELAY);
                fadeOut.setDuration(FADE_DURATION);
                mScrollView.clearAnimation();
                mScrollView.startAnimation(fadeOut);
            }
        }

        private void addSectionHeader(int actualSection) {
            View previousHeader = mHeader.getChildAt(0);
            if (previousHeader != null) {
                mHeader.removeViewAt(0);
            }

            if (mAdapter.hasSectionHeaderView(actualSection)) {
                mHeaderConvertView = mAdapter.getSectionHeaderView(actualSection, mHeaderConvertView, mHeader);
                mHeaderConvertView.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));

                mHeaderConvertView.measure(MeasureSpec.makeMeasureSpec(mHeader.getWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));

                mHeader.getLayoutParams().height = mHeaderConvertView.getMeasuredHeight();
                mHeaderConvertView.scrollTo(0, 0);
                mHeader.scrollTo(0, 0);
                mHeader.addView(mHeaderConvertView, 0);
            } else {
                mHeader.getLayoutParams().height = 0;
                mHeader.scrollTo(0, 0);
            }

            mScrollView.bringToFront();
        }

        private int getRealFirstVisibleItem(int firstVisibleItem, int visibleItemCount) {
            if (visibleItemCount == 0) {
                return -1;
            }
            int relativeIndex = 0, totalHeight = mListView.getChildAt(0).getTop();
            for (relativeIndex = 0; relativeIndex < visibleItemCount && totalHeight < mHeader.getHeight(); relativeIndex++) {
                totalHeight += mListView.getChildAt(relativeIndex).getHeight();
            }
            int realFVI = Math.max(firstVisibleItem, firstVisibleItem + relativeIndex - 1);
            return realFVI;
        }
    }

    public ListView getListView() {
        return mListView;
    }

    public void addHeaderView(View v) {
        mListView.addHeaderView(v);
    }

    private float dpToPx(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getContext().getResources().getDisplayMetrics());
    }

    protected class InternalListView extends ListView {

        public InternalListView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        @Override
        protected int computeVerticalScrollExtent() {
            return super.computeVerticalScrollExtent();
        }

        @Override
        protected int computeVerticalScrollOffset() {
            return super.computeVerticalScrollOffset();
        }

        @Override
        protected int computeVerticalScrollRange() {
            return super.computeVerticalScrollRange();
        }
    }
}

XML usage

XML 用法

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context=".MainActivity">

    <com.example.parsaniahardik.listview_stickyheader_ios.HeaderListView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:id="@+id/lv">

    </com.example.parsaniahardik.listview_stickyheader_ios.HeaderListView>