How to make sticky section headers (like iOS) in Android?
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 sticky section headers in Android.
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.
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():
return actualCount - 1;
and make sure the first line in your getView() method is
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
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>
Add this in your app.gradle file
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.
<?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)
<?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>