I want to fix my header views in the top of the screen like in the image below and without using external libraries.
In my case, I don't want to do it alphabetically. I have two different types of views (Header and normal). I only want to fix to the top, the last header.
Here I will explain how to do it without an external library. It will be a very long post, so brace yourself.
First of all, let me acknowledge #tim.paetz whose post inspired me to set off to a journey of implementing my own sticky headers using ItemDecorations. I borrowed some parts of his code in my implementation.
As you might have already experienced, if you attempted to do it yourself, it is very hard to find a good explanation of HOW to actually do it with the ItemDecoration technique. I mean, what are the steps? What is the logic behind it? How do I make the header stick on top of the list? Not knowing answers to these questions is what makes others to use external libraries, while doing it yourself with the use of ItemDecoration is pretty easy.
Initial conditions
You dataset should be a list of items of different type (not in a "Java types" sense, but in a "header/item" types sense).
Your list should be already sorted.
Every item in the list should be of certain type - there should be a header item related to it.
Very first item in the list must be a header item.
Here I provide full code for my RecyclerView.ItemDecoration called HeaderItemDecoration. Then I explain the steps taken in detail.
public class HeaderItemDecoration extends RecyclerView.ItemDecoration {
private StickyHeaderInterface mListener;
private int mStickyHeaderHeight;
public HeaderItemDecoration(RecyclerView recyclerView, #NonNull StickyHeaderInterface listener) {
mListener = listener;
// On Sticky Header Click
recyclerView.addOnItemTouchListener(new RecyclerView.OnItemTouchListener() {
public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
if (motionEvent.getY() <= mStickyHeaderHeight) {
// Handle the clicks on the header here ...
return true;
}
return false;
}
public void onTouchEvent(RecyclerView recyclerView, MotionEvent motionEvent) {
}
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
}
});
}
#Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
View topChild = parent.getChildAt(0);
if (Util.isNull(topChild)) {
return;
}
int topChildPosition = parent.getChildAdapterPosition(topChild);
if (topChildPosition == RecyclerView.NO_POSITION) {
return;
}
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
fixLayoutSize(parent, currentHeader);
int contactPoint = currentHeader.getBottom();
View childInContact = getChildInContact(parent, contactPoint);
if (Util.isNull(childInContact)) {
return;
}
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
return;
}
drawHeader(c, currentHeader);
}
private View getHeaderViewForItem(int itemPosition, RecyclerView parent) {
int headerPosition = mListener.getHeaderPositionForItem(itemPosition);
int layoutResId = mListener.getHeaderLayout(headerPosition);
View header = LayoutInflater.from(parent.getContext()).inflate(layoutResId, parent, false);
mListener.bindHeaderData(header, headerPosition);
return header;
}
private void drawHeader(Canvas c, View header) {
c.save();
c.translate(0, 0);
header.draw(c);
c.restore();
}
private void moveHeader(Canvas c, View currentHeader, View nextHeader) {
c.save();
c.translate(0, nextHeader.getTop() - currentHeader.getHeight());
currentHeader.draw(c);
c.restore();
}
private View getChildInContact(RecyclerView parent, int contactPoint) {
View childInContact = null;
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
if (child.getBottom() > contactPoint) {
if (child.getTop() <= contactPoint) {
// This child overlaps the contactPoint
childInContact = child;
break;
}
}
}
return childInContact;
}
/**
* Properly measures and layouts the top sticky header.
* #param parent ViewGroup: RecyclerView in this case.
*/
private void fixLayoutSize(ViewGroup parent, View view) {
// Specs for parent (RecyclerView)
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED);
// Specs for children (headers)
int childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.getPaddingLeft() + parent.getPaddingRight(), view.getLayoutParams().width);
int childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.getPaddingTop() + parent.getPaddingBottom(), view.getLayoutParams().height);
view.measure(childWidthSpec, childHeightSpec);
view.layout(0, 0, view.getMeasuredWidth(), mStickyHeaderHeight = view.getMeasuredHeight());
}
public interface StickyHeaderInterface {
/**
* This method gets called by {#link HeaderItemDecoration} to fetch the position of the header item in the adapter
* that is used for (represents) item at specified position.
* #param itemPosition int. Adapter's position of the item for which to do the search of the position of the header item.
* #return int. Position of the header item in the adapter.
*/
int getHeaderPositionForItem(int itemPosition);
/**
* This method gets called by {#link HeaderItemDecoration} to get layout resource id for the header item at specified adapter's position.
* #param headerPosition int. Position of the header item in the adapter.
* #return int. Layout resource id.
*/
int getHeaderLayout(int headerPosition);
/**
* This method gets called by {#link HeaderItemDecoration} to setup the header View.
* #param header View. Header to set the data on.
* #param headerPosition int. Position of the header item in the adapter.
*/
void bindHeaderData(View header, int headerPosition);
/**
* This method gets called by {#link HeaderItemDecoration} to verify whether the item represents a header.
* #param itemPosition int.
* #return true, if item at the specified adapter's position represents a header.
*/
boolean isHeader(int itemPosition);
}
}
Business logic
So, how do I make it stick?
You don't. You can't make a RecyclerView's item of your choice just stop and stick on top, unless you are a guru of custom layouts and you know 12,000+ lines of code for a RecyclerView by heart. So, as it always goes with the UI design, if you can't make something, fake it. You just draw the header on top of everything using Canvas. You also should know which items the user can see at the moment. It just happens, that ItemDecoration can provide you with both the Canvas and information about visible items. With this, here are basic steps:
In onDrawOver method of RecyclerView.ItemDecoration get the very first (top) item that is visible to the user.
View topChild = parent.getChildAt(0);
Determine which header represents it.
int topChildPosition = parent.getChildAdapterPosition(topChild);
View currentHeader = getHeaderViewForItem(topChildPosition, parent);
Draw the appropriate header on top of the RecyclerView by using drawHeader() method.
I also want to implement the behavior when the new upcoming header meets the top one: it should seem as the upcoming header gently pushes the top current header out of the view and takes his place eventually.
Same technique of "drawing on top of everything" applies here.
Determine when the top "stuck" header meets the new upcoming one.
View childInContact = getChildInContact(parent, contactPoint);
Get this contact point (that is the bottom of the sticky header your drew and the top of the upcoming header).
int contactPoint = currentHeader.getBottom();
If the item in the list is trespassing this "contact point", redraw your sticky header so its bottom will be at the top of the trespassing item. You achieve this with translate() method of the Canvas. As the result, the starting point of the top header will be out of visible area, and it will seem as "being pushed out by the upcoming header". When it is completely gone, draw the new header on top.
if (childInContact != null) {
if (mListener.isHeader(parent.getChildAdapterPosition(childInContact))) {
moveHeader(c, currentHeader, childInContact);
} else {
drawHeader(c, currentHeader);
}
}
The rest is explained by comments and thorough annotations in piece of code I provided.
The usage is straight forward:
mRecyclerView.addItemDecoration(new HeaderItemDecoration((HeaderItemDecoration.StickyHeaderInterface) mAdapter));
Your mAdapter must implement StickyHeaderInterface for it to work. The implementation depends on the data you have.
Finally, here I provide a gif with a half-transparent headers, so you can grasp the idea and actually see what is going on under the hood.
Here is the illustration of "just draw on top of everything" concept. You can see that there are two items "header 1" - one that we draw and stays on top in a stuck position, and the other one that comes from the dataset and moves with all the rest items. The user won't see the inner-workings of it, because you'll won't have half-transparent headers.
And here what happens in the "pushing out" phase:
Hope it helped.
Edit
Here is my actual implementation of getHeaderPositionForItem() method in the RecyclerView's adapter:
#Override
public int getHeaderPositionForItem(int itemPosition) {
int headerPosition = 0;
do {
if (this.isHeader(itemPosition)) {
headerPosition = itemPosition;
break;
}
itemPosition -= 1;
} while (itemPosition >= 0);
return headerPosition;
}
Slightly different implementation in Kotlin
Easiest way is to just create an Item Decoration for your RecyclerView.
import android.graphics.Canvas;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
public class RecyclerSectionItemDecoration extends RecyclerView.ItemDecoration {
private final int headerOffset;
private final boolean sticky;
private final SectionCallback sectionCallback;
private View headerView;
private TextView header;
public RecyclerSectionItemDecoration(int headerHeight, boolean sticky, #NonNull SectionCallback sectionCallback) {
headerOffset = headerHeight;
this.sticky = sticky;
this.sectionCallback = sectionCallback;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int pos = parent.getChildAdapterPosition(view);
if (sectionCallback.isSection(pos)) {
outRect.top = headerOffset;
}
}
#Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c,
parent,
state);
if (headerView == null) {
headerView = inflateHeaderView(parent);
header = (TextView) headerView.findViewById(R.id.list_item_section_text);
fixLayoutSize(headerView,
parent);
}
CharSequence previousHeader = "";
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
final int position = parent.getChildAdapterPosition(child);
CharSequence title = sectionCallback.getSectionHeader(position);
header.setText(title);
if (!previousHeader.equals(title) || sectionCallback.isSection(position)) {
drawHeader(c,
child,
headerView);
previousHeader = title;
}
}
}
private void drawHeader(Canvas c, View child, View headerView) {
c.save();
if (sticky) {
c.translate(0,
Math.max(0,
child.getTop() - headerView.getHeight()));
} else {
c.translate(0,
child.getTop() - headerView.getHeight());
}
headerView.draw(c);
c.restore();
}
private View inflateHeaderView(RecyclerView parent) {
return LayoutInflater.from(parent.getContext())
.inflate(R.layout.recycler_section_header,
parent,
false);
}
/**
* Measures the header view to make sure its size is greater than 0 and will be drawn
* https://yoda.entelect.co.za/view/9627/how-to-android-recyclerview-item-decorations
*/
private void fixLayoutSize(View view, ViewGroup parent) {
int widthSpec = View.MeasureSpec.makeMeasureSpec(parent.getWidth(),
View.MeasureSpec.EXACTLY);
int heightSpec = View.MeasureSpec.makeMeasureSpec(parent.getHeight(),
View.MeasureSpec.UNSPECIFIED);
int childWidth = ViewGroup.getChildMeasureSpec(widthSpec,
parent.getPaddingLeft() + parent.getPaddingRight(),
view.getLayoutParams().width);
int childHeight = ViewGroup.getChildMeasureSpec(heightSpec,
parent.getPaddingTop() + parent.getPaddingBottom(),
view.getLayoutParams().height);
view.measure(childWidth,
childHeight);
view.layout(0,
0,
view.getMeasuredWidth(),
view.getMeasuredHeight());
}
public interface SectionCallback {
boolean isSection(int position);
CharSequence getSectionHeader(int position);
}
}
XML for your header in recycler_section_header.xml:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/list_item_section_text"
android:layout_width="match_parent"
android:layout_height="#dimen/recycler_section_header_height"
android:background="#android:color/black"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:textColor="#android:color/white"
android:textSize="14sp"
/>
And finally to add the Item Decoration to your RecyclerView:
RecyclerSectionItemDecoration sectionItemDecoration =
new RecyclerSectionItemDecoration(getResources().getDimensionPixelSize(R.dimen.recycler_section_header_height),
true, // true for sticky, false for not
new RecyclerSectionItemDecoration.SectionCallback() {
#Override
public boolean isSection(int position) {
return position == 0
|| people.get(position)
.getLastName()
.charAt(0) != people.get(position - 1)
.getLastName()
.charAt(0);
}
#Override
public CharSequence getSectionHeader(int position) {
return people.get(position)
.getLastName()
.subSequence(0,
1);
}
});
recyclerView.addItemDecoration(sectionItemDecoration);
With this Item Decoration you can either make the header pinned/sticky or not with just a boolean when creating the Item Decoration.
You can find a complete working example on github: https://github.com/paetztm/recycler_view_headers
I've made my own variation of Sevastyan's solution above
class HeaderItemDecoration(recyclerView: RecyclerView, private val listener: StickyHeaderInterface) : RecyclerView.ItemDecoration() {
private val headerContainer = FrameLayout(recyclerView.context)
private var stickyHeaderHeight: Int = 0
private var currentHeader: View? = null
private var currentHeaderPosition = 0
init {
val layout = RelativeLayout(recyclerView.context)
val params = recyclerView.layoutParams
val parent = recyclerView.parent as ViewGroup
val index = parent.indexOfChild(recyclerView)
parent.addView(layout, index, params)
parent.removeView(recyclerView)
layout.addView(recyclerView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
layout.addView(headerContainer, LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
val topChild = parent.getChildAt(0) ?: return
val topChildPosition = parent.getChildAdapterPosition(topChild)
if (topChildPosition == RecyclerView.NO_POSITION) {
return
}
val currentHeader = getHeaderViewForItem(topChildPosition, parent)
fixLayoutSize(parent, currentHeader)
val contactPoint = currentHeader.bottom
val childInContact = getChildInContact(parent, contactPoint) ?: return
val nextPosition = parent.getChildAdapterPosition(childInContact)
if (listener.isHeader(nextPosition)) {
moveHeader(currentHeader, childInContact, topChildPosition, nextPosition)
return
}
drawHeader(currentHeader, topChildPosition)
}
private fun getHeaderViewForItem(itemPosition: Int, parent: RecyclerView): View {
val headerPosition = listener.getHeaderPositionForItem(itemPosition)
val layoutResId = listener.getHeaderLayout(headerPosition)
val header = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false)
listener.bindHeaderData(header, headerPosition)
return header
}
private fun drawHeader(header: View, position: Int) {
headerContainer.layoutParams.height = stickyHeaderHeight
setCurrentHeader(header, position)
}
private fun moveHeader(currentHead: View, nextHead: View, currentPos: Int, nextPos: Int) {
val marginTop = nextHead.top - currentHead.height
if (currentHeaderPosition == nextPos && currentPos != nextPos) setCurrentHeader(currentHead, currentPos)
val params = currentHeader?.layoutParams as? MarginLayoutParams ?: return
params.setMargins(0, marginTop, 0, 0)
currentHeader?.layoutParams = params
headerContainer.layoutParams.height = stickyHeaderHeight + marginTop
}
private fun setCurrentHeader(header: View, position: Int) {
currentHeader = header
currentHeaderPosition = position
headerContainer.removeAllViews()
headerContainer.addView(currentHeader)
}
private fun getChildInContact(parent: RecyclerView, contactPoint: Int): View? =
(0 until parent.childCount)
.map { parent.getChildAt(it) }
.firstOrNull { it.bottom > contactPoint && it.top <= contactPoint }
private fun fixLayoutSize(parent: ViewGroup, view: View) {
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec,
parent.paddingLeft + parent.paddingRight,
view.layoutParams.width)
val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec,
parent.paddingTop + parent.paddingBottom,
view.layoutParams.height)
view.measure(childWidthSpec, childHeightSpec)
stickyHeaderHeight = view.measuredHeight
view.layout(0, 0, view.measuredWidth, stickyHeaderHeight)
}
interface StickyHeaderInterface {
fun getHeaderPositionForItem(itemPosition: Int): Int
fun getHeaderLayout(headerPosition: Int): Int
fun bindHeaderData(header: View, headerPosition: Int)
fun isHeader(itemPosition: Int): Boolean
}
}
... and here is implementation of StickyHeaderInterface (I did it directly in recycler adapter):
override fun getHeaderPositionForItem(itemPosition: Int): Int =
(itemPosition downTo 0)
.map { Pair(isHeader(it), it) }
.firstOrNull { it.first }?.second ?: RecyclerView.NO_POSITION
override fun getHeaderLayout(headerPosition: Int): Int {
/* ...
return something like R.layout.view_header
or add conditions if you have different headers on different positions
... */
}
override fun bindHeaderData(header: View, headerPosition: Int) {
if (headerPosition == RecyclerView.NO_POSITION) header.layoutParams.height = 0
else /* ...
here you get your header and can change some data on it
... */
}
override fun isHeader(itemPosition: Int): Boolean {
/* ...
here have to be condition for checking - is item on this position header
... */
}
So, in this case header is not just drawing on canvas, but view with selector or ripple, clicklistener, etc.
to anyone looking for solution to the flickering/blinking issue when you already have DividerItemDecoration. i seem to have solved it like this:
override fun onDrawOver(...)
{
//code from before
//do NOT return on null
val childInContact = getChildInContact(recyclerView, currentHeader.bottom)
//add null check
if (childInContact != null && mHeaderListener.isHeader(recyclerView.getChildAdapterPosition(childInContact)))
{
moveHeader(...)
return
}
drawHeader(...)
}
this seems to be working but can anyone confirm i did not break anything else?
You can check and take the implementation of the class StickyHeaderHelper in my FlexibleAdapter project, and adapt it to your use case.
But, I suggest to use the library since it simplifies and reorganizes the way you usually implement the Adapters for RecyclerView: Don't reinvent the wheel.
I would also say, don't use Decorators or deprecated libraries, as well as don't use libraries that do only 1 or 3 things, you will have to merge implementations of others libraries yourself.
Yo,
This is how you do it if you want just one type of holder stick when it starts getting out of the screen (we are not caring about any sections). There is only one way without breaking the internal RecyclerView logic of recycling items and that is to inflate additional view on top of the recyclerView's header item and pass data into it. I'll let the code speak.
import android.graphics.Canvas
import android.graphics.Rect
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView
class StickyHeaderItemDecoration(#LayoutRes private val headerId: Int, private val HEADER_TYPE: Int) : RecyclerView.ItemDecoration() {
private lateinit var stickyHeaderView: View
private lateinit var headerView: View
private var sticked = false
// executes on each bind and sets the stickyHeaderView
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
val position = parent.getChildAdapterPosition(view)
val adapter = parent.adapter ?: return
val viewType = adapter.getItemViewType(position)
if (viewType == HEADER_TYPE) {
headerView = view
}
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
if (::headerView.isInitialized) {
if (headerView.y <= 0 && !sticked) {
stickyHeaderView = createHeaderView(parent)
fixLayoutSize(parent, stickyHeaderView)
sticked = true
}
if (headerView.y > 0 && sticked) {
sticked = false
}
if (sticked) {
drawStickedHeader(c)
}
}
}
private fun createHeaderView(parent: RecyclerView) = LayoutInflater.from(parent.context).inflate(headerId, parent, false)
private fun drawStickedHeader(c: Canvas) {
c.save()
c.translate(0f, Math.max(0f, stickyHeaderView.top.toFloat() - stickyHeaderView.height.toFloat()))
headerView.draw(c)
c.restore()
}
private fun fixLayoutSize(parent: ViewGroup, view: View) {
// Specs for parent (RecyclerView)
val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.UNSPECIFIED)
// Specs for children (headers)
val childWidthSpec = ViewGroup.getChildMeasureSpec(widthSpec, parent.paddingLeft + parent.paddingRight, view.getLayoutParams().width)
val childHeightSpec = ViewGroup.getChildMeasureSpec(heightSpec, parent.paddingTop + parent.paddingBottom, view.getLayoutParams().height)
view.measure(childWidthSpec, childHeightSpec)
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
}
}
And then you just do this in your adapter:
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
recyclerView.addItemDecoration(StickyHeaderItemDecoration(R.layout.item_time_filter, YOUR_STICKY_VIEW_HOLDER_TYPE))
}
Where YOUR_STICKY_VIEW_HOLDER_TYPE is viewType of your what is supposed to be sticky holder.
Another solution, based on scroll listener. Initial conditions are the same as in Sevastyan answer
RecyclerView recyclerView;
TextView tvTitle; //sticky header view
//... onCreate, initialize, etc...
public void bindList(List<Item> items) { //All data in adapter. Item - just interface for different item types
adapter = new YourAdapter(items);
recyclerView.setAdapter(adapter);
StickyHeaderViewManager<HeaderItem> stickyHeaderViewManager = new StickyHeaderViewManager<>(
tvTitle,
recyclerView,
HeaderItem.class, //HeaderItem - subclass of Item, used to detect headers in list
data -> { // bind function for sticky header view
tvTitle.setText(data.getTitle());
});
stickyHeaderViewManager.attach(items);
}
Layout for ViewHolder and sticky header.
item_header.xml
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/tv_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
Layout for RecyclerView
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="#+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!--it can be any view, but order important, draw over recyclerView-->
<include
layout="#layout/item_header"/>
</FrameLayout>
Class for HeaderItem.
public class HeaderItem implements Item {
private String title;
public HeaderItem(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
}
It's all use. The implementation of the adapter, ViewHolder and other things, is not interesting for us.
public class StickyHeaderViewManager<T> {
#Nonnull
private View headerView;
#Nonnull
private RecyclerView recyclerView;
#Nonnull
private StickyHeaderViewWrapper<T> viewWrapper;
#Nonnull
private Class<T> headerDataClass;
private List<?> items;
public StickyHeaderViewManager(#Nonnull View headerView,
#Nonnull RecyclerView recyclerView,
#Nonnull Class<T> headerDataClass,
#Nonnull StickyHeaderViewWrapper<T> viewWrapper) {
this.headerView = headerView;
this.viewWrapper = viewWrapper;
this.recyclerView = recyclerView;
this.headerDataClass = headerDataClass;
}
public void attach(#Nonnull List<?> items) {
this.items = items;
if (ViewCompat.isLaidOut(headerView)) {
bindHeader(recyclerView);
} else {
headerView.post(() -> bindHeader(recyclerView));
}
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
bindHeader(recyclerView);
}
});
}
private void bindHeader(RecyclerView recyclerView) {
if (items.isEmpty()) {
headerView.setVisibility(View.GONE);
return;
} else {
headerView.setVisibility(View.VISIBLE);
}
View topView = recyclerView.getChildAt(0);
if (topView == null) {
return;
}
int topPosition = recyclerView.getChildAdapterPosition(topView);
if (!isValidPosition(topPosition)) {
return;
}
if (topPosition == 0 && topView.getTop() == recyclerView.getTop()) {
headerView.setVisibility(View.GONE);
return;
} else {
headerView.setVisibility(View.VISIBLE);
}
T stickyItem;
Object firstItem = items.get(topPosition);
if (headerDataClass.isInstance(firstItem)) {
stickyItem = headerDataClass.cast(firstItem);
headerView.setTranslationY(0);
} else {
stickyItem = findNearestHeader(topPosition);
int secondPosition = topPosition + 1;
if (isValidPosition(secondPosition)) {
Object secondItem = items.get(secondPosition);
if (headerDataClass.isInstance(secondItem)) {
View secondView = recyclerView.getChildAt(1);
if (secondView != null) {
moveViewFor(secondView);
}
} else {
headerView.setTranslationY(0);
}
}
}
if (stickyItem != null) {
viewWrapper.bindView(stickyItem);
}
}
private void moveViewFor(View secondView) {
if (secondView.getTop() <= headerView.getBottom()) {
headerView.setTranslationY(secondView.getTop() - headerView.getHeight());
} else {
headerView.setTranslationY(0);
}
}
private T findNearestHeader(int position) {
for (int i = position; position >= 0; i--) {
Object item = items.get(i);
if (headerDataClass.isInstance(item)) {
return headerDataClass.cast(item);
}
}
return null;
}
private boolean isValidPosition(int position) {
return !(position == RecyclerView.NO_POSITION || position >= items.size());
}
}
Interface for bind header view.
public interface StickyHeaderViewWrapper<T> {
void bindView(T data);
}
For those who may concern. Based on Sevastyan's answer, should you want to make it horizontal scroll.
Simply change all getBottom() to getRight() and getTop() to getLeft()
you can get sticky header functionality by copying these 2 files into your project. i had no issues with this implementation:
can interact with the sticy header (tap/long press/swipe)
the sticky header hides and reveals itself properly...even if each view holder has a different height (some other answers here don't handle that properly, causing the wrong headers to show, or the headers to jump up and down)
see an example of the 2 files being used in this small github project i whipped up
In case you want the header to be beside your recyclerview item like this
then use the same code here
and add this two lines inside onDrawOver
//hide the image and the name, and draw only the alphabet
val headerView = getHeaderViewForItem(topChildPosition, parent) ?: return
headerView.findViewById<ShapeableImageView>(R.id.contactImageView).isVisible = false
headerView.findViewById<TextView>(R.id.nameTextView).isVisible = false
here you are basically redrawing again the recyclerview item but hiding all elements which is on the right.
if you are wondering how to create such recyclerview item, then here is how:
then you will create list of your data like this:
class ContactRecyclerDataItem(val contact: SimpleContact, val alphabet: String? = null)
so that when you recieve the list of your data you can build list of ContactRecyclerDataItem
this way
list?.let {
val adapterDataList = mutableListOf<ContactRecyclerDataItem>()
if (it.isNotEmpty()) {
var prevChar = (it[0].name[0].code + 1).toChar()
it.forEach { contact ->
if (contact.name[0] != prevChar) {
prevChar = contact.name[0]
adapterDataList.add(ContactRecyclerDataItem(contact, prevChar.toString()))
} else {
adapterDataList.add(ContactRecyclerDataItem(contact))
}
}
}
contactsAdapter.data = adapterDataList
}
then inside your recycler adapter inside viewHolder you make check if the alphabet is empty or not,
if (itemRecycler.alphabet != null) {
alphabetTextView.text = itemRecycler.alphabet
} else {
alphabetTextView.text = ""
}
at the end you build this recyclerview with alphabets on the left, but to make them sticky you inflate and move the first element which is the header all the way down until the next header, the trick as mentioned above is to hide all the other elements in your recyclerview item except the alphabet.
to make the first element clickable you should return false inside the itemDecorat
inside init block in parent.addOnItemTouchListene{}
when returning false, you are passing the click listener to the bellow view which is in this case your visible recyclerview item.
The answer has already been here. If you don't want to use any library, you can follow these steps:
Sort list with data by name
Iterate via list with data, and in place when current's item first letter != first letter of next item, insert "special" kind of object.
Inside your Adapter place special view when item is "special".
Explanation:
In onCreateViewHolder method we can check viewType and depending on the value (our "special" kind) inflate a special layout.
For example:
public static final int TITLE = 0;
public static final int ITEM = 1;
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (context == null) {
context = parent.getContext();
}
if (viewType == TITLE) {
view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_title, parent,false);
return new TitleElement(view);
} else if (viewType == ITEM) {
view = LayoutInflater.from(context).inflate(R.layout.recycler_adapter_item, parent,false);
return new ItemElement(view);
}
return null;
}
where class ItemElement and class TitleElement can look like ordinary ViewHolder :
public class ItemElement extends RecyclerView.ViewHolder {
//TextView text;
public ItemElement(View view) {
super(view);
//text = (TextView) view.findViewById(R.id.text);
}
So the idea of all of that is interesting. But i am interested if it's effectively, cause we need to sort the data list. And i think this will take the speed down. If any thoughts about it, please write me :)
And also the opened question : is how to hold the "special" layout on the top, while the items are recycling. Maybe combine all of that with CoordinatorLayout.
Related
How can I snap to particular position for LinearSnapHelper() in horizontal RecyclerView? There is a function scrolltoposition for RecyclerView which scroll to that position but did not keep it in center for this snaphelper.
I am looking for something like below image. So when I set to particular position, it will keep it in center. I dont find anything related to select position for SnapHelper
i find this , but this doesn't help me.
Any help would be appreciated.
If I understand your question, you are looking for a way to jump to a position and have that position centered in the RecyclerView.
Maybe you have tried RecyclerView.scrollToPosition() but that doesn't snap to the view. You may also have tried RecyclerView.smoothScrollToPosition() and that works better but you may want to avoid all the movement if you have a lot of items and are scrolling a long way.
The reason that scrollToPosition() doesn't work is that it doesn't trigger the LinearSnapHelper which uses a scroll listener to detect when to snap. Since smoothScrollToPosition() does trigger the LinearSnapHelper, we will use scrollToPosition() to get us in the area of the target view then use smoothScrollToPosition() to get the view centered as follows:
private RecyclerView mRecycler;
private void newScrollTo(final int pos) {
RecyclerView.ViewHolder vh = mRecycler.findViewHolderForLayoutPosition(pos);
if (vh != null) {
// Target view is available, so just scroll to it.
mRecycler.smoothScrollToPosition(pos);
} else {
// Target view is not available. Scroll to it.
mRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
// From the documentation:
// This callback will also be called if visible item range changes after a layout
// calculation. In that case, dx and dy will be 0.This callback will also be called
// if visible item range changes after a layout calculation. In that case,
// dx and dy will be 0.
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mRecycler.removeOnScrollListener(this);
if (dx == 0) {
newScrollTo(pos);
}
}
});
mRecycler.scrollToPosition(pos);
}
}
Sample app
MainActivity.java
public class MainActivity extends AppCompatActivity {
private final LinearLayoutManager mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false);
private final List<String> mItems = new ArrayList<>();
private RecyclerView mRecycler;
private final int mItemCount = 2000;
private final Handler mHandler = new Handler();
private final LinearSnapHelper mLinearSnapHelper = new LinearSnapHelper();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
for (int i = 0; i < mItemCount; i++) {
mItems.add(i + "");
}
mRecycler = findViewById(R.id.recyclerView);
final RecyclerViewAdapter adapter = new RecyclerViewAdapter(null);
adapter.setItems(mItems);
mRecycler.setLayoutManager(mLayoutManager);
mRecycler.setAdapter(adapter);
mLinearSnapHelper.attachToRecyclerView(mRecycler);
newScrollTo(1);
// fireScrollTo();
}
private int maxScrolls = mItemCount;
private void fireScrollTo() {
if (--maxScrolls > 0) {
int pos = (int) (Math.random() * mItemCount);
newScrollTo(pos);
mHandler.postDelayed(new Runnable() {
#Override
public void run() {
fireScrollTo();
}
}, 2000);
}
}
private void newScrollTo(final int pos) {
mRecycler.smoothScrollToPosition(pos);
RecyclerView.ViewHolder vh = mRecycler.findViewHolderForLayoutPosition(pos);
if (vh != null) {
// Target view is available, so just scroll to it.
mRecycler.smoothScrollToPosition(pos);
} else {
// Target view is not available. Scroll to it.
mRecycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
// From the documentation:
// This callback will also be called if visible item range changes after a layout
// calculation. In that case, dx and dy will be 0.This callback will also be called
// if visible item range changes after a layout calculation. In that case,
// dx and dy will be 0.
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
mRecycler.removeOnScrollListener(this);
if (dx == 0) {
newScrollTo(pos);
}
}
});
mRecycler.scrollToPosition(pos);
}
}
}
activity_main.xml
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
android:layout_width="3px"
android:layout_height="match_parent"
android:background="#android:color/holo_red_light"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.v7.widget.RecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingStart="660px"
android:paddingEnd="660px"/>
</android.support.constraint.ConstraintLayout>
RecyclerViewAdapter.java
class RecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private List<String> mItems;
RecyclerViewAdapter(List<String> items) {
mItems = items;
}
#Override
public #NonNull
RecyclerView.ViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);
view.getLayoutParams().width = 220;
view.getLayoutParams().height = 220;
// view.setPadding(220 * 3, 0, 220 * 3, 0);
((TextView) view).setGravity(Gravity.CENTER);
return new ItemViewHolder(view);
}
#Override
public void onBindViewHolder(#NonNull RecyclerView.ViewHolder holder, int position) {
ItemViewHolder vh = (ItemViewHolder) holder;
String itemText = mItems.get(position);
vh.mItemTextView.setText(itemText);
int bgColor = (position % 2 == 0)
? android.R.color.holo_blue_light
: android.R.color.holo_green_light;
holder.itemView.setBackgroundColor(
holder.itemView.getContext().getResources().getColor(bgColor));
}
#Override
public int getItemCount() {
return (mItems == null) ? 0 : mItems.size();
}
#Override
public int getItemViewType(int position) {
return TYPE_ITEM;
}
static class ItemViewHolder extends RecyclerView.ViewHolder {
private TextView mItemTextView;
ItemViewHolder(View item) {
super(item);
mItemTextView = item.findViewById(android.R.id.text1);
}
}
public void setItems(List<String> items) {
mItems = items;
}
#SuppressWarnings("unused")
private final static String TAG = "RecyclerViewAdapter";
private final static int TYPE_ITEM = 1;
}
Add this where ever you want to scroll your recycler view
recyclerView.scrollToPosition(position)
recyclerView.post {
var view = recyclerView.layoutManager?.findViewByPosition(position);
if (view == null) {
// do nothing
}
var snapDistance = snapHelper.calculateDistanceToFinalSnap(recyclerView.layoutManager!!, view!!)
if (snapDistance?.get(0) != 0 || snapDistance[1] != 0) {
recyclerView.scrollBy(snapDistance?.get(0)!!, snapDistance?.get(1));
}
}
By using this LinearSnap Helper which is attached to your recycler view
var snapHelper = LinearSnapHelper()
snapHelper.attachToRecyclerView(recyclerView)
For horizontal RecyclerView, you should use PagerSnapHelper() instead of LinearSnapHelper().
Just did some research and trace the SnapHelper's source code, it turns out the solution could be very simple:
class MyPagerSnapHelper: PagerSnapHelper() {
fun smoothScrollToPosition(layoutManager: RecyclerView.LayoutManager, position: Int) {
val smoothScroller = createScroller(layoutManager) ?: return
smoothScroller.targetPosition = position
layoutManager.startSmoothScroll(smoothScroller)
}
}
And then you can pass RecyclerView's LayoutManager and target position here
snapHelper.smoothScrollToPosition(recyclerView.layoutManager!!, index)
I need to add a separator only to the absolute first item of my recycle view.
I have already read How to selectively decorate RecyclerView items , and i understand that
The (onDraw) method loops over all the child views currently in the RecyclerView visible on the screen.
and my problem is exactly that. since it executes every time the views in the RecycleView change, even if i am able to locate and decorate only the first item, as soon as i scroll down, the decoration shifts to the current first item.
In that link the selection is done by the method isDecorated which looks at the instance of the current child's ViewHolder. My guess is that the guy wanted to decorate with respect to the ViewHolder type, which is not my problem since i have only one type of element in my RecyclerView
This is my DividerItemDecoration.java
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable mDivider;
public DividerItemDecoration(Drawable divider) {
mDivider = divider;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
RecyclerView.ViewHolder holder = parent.getChildViewHolder(view);
if (parent.getChildAdapterPosition(view) == 1) {
outRect.top = outRect.top + mDivider.getIntrinsicHeight();
}
return;
}
#Override
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
int dividerLeft = parent.getPaddingLeft();
int dividerRight = parent.getWidth() - parent.getPaddingRight();
int childCount = parent.getChildCount();
for (int i = 0; i < childCount - 1; i++) {
if(i==0 ){
View child = parent.getChildAt(i);
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
int dividerTop = child.getBottom() + params.bottomMargin;
int dividerBottom = dividerTop + mDivider.getIntrinsicHeight();
mDivider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom);
mDivider.draw(canvas);
}
}
}
}
Please consider that i have searched all day and could only find examples and gists to add decorator to every item, to selectively add it based on the type, but nothing really tackling with the absolute position.
Also, please no libraries like https://github.com/yqritc/RecyclerView-FlexibleDivider, i want to learn , not to copy.
You can check if it's the first element,
Here's an example:
Just implement getItemViewType(), and take care of the viewType parameter in onCreateViewHolder().
So you do something like:
#Override
public int getItemViewType(int position) {
if (position == 0)
return 1;
else
return 2;
}
then in onCreateViewHolder inflate your different layout according to your viewType.
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == 1) {
// inflate your first item layout & return that viewHolder
} else {
// inflate your other item layout & return that viewHolder
}
}
Background
Suppose I have a vertical RecyclerView, where each row is a horizontal RecyclerView.
What I'd like to do is that no matter which horizontal RecyclerViews you scroll, all of the others will scroll accordingly, and always be synced with the exact same scroll X coordinate
The problem
I actually did ok for the basic operation :
It works by having a scrolling listener that all horizontal RecyclerViews have, yet when one starts to scroll, it is the only one that will have it, while it also affects the others to scroll with it.
However, I have 2 main issues with what I did:
In some (horizontal) scrolling operations (maybe some gestures, like fling), the scrolling of the multiple RecyclerViews is out of sync, so some are in X coordinate that is different from the others.
When scrolling vertically, I couldn't succeed setting the X coordinate correctly. Not only that, but onBindViewHolder of the vertical RecyclerView doesn't get called when I expected it to be called (called when I scroll a lot, and not just when I see a used one being re-shown).
What I've tried
Here's the current code:
MainActivity.java
public class MainActivity extends AppCompatActivity {
int mCurX = 0;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final RecyclerView mainRecyclerView = (RecyclerView) findViewById(R.id.activity_main);
final LinearLayoutManager verticalLinearLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
mainRecyclerView.setLayoutManager(verticalLinearLayoutManager);
final LayoutInflater layoutInflater = LayoutInflater.from(this);
final OnScrollListener masterOnScrollListener = new OnScrollListener() {
RecyclerView masterRecyclerView = null;
#Override
public void onScrollStateChanged(final RecyclerView recyclerView, final int newState) {
super.onScrollStateChanged(recyclerView, newState);
switch (newState) {
case RecyclerView.SCROLL_STATE_IDLE:
if (masterRecyclerView != null) {
masterRecyclerView = null;
final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
if (horizontalRecyclerView != recyclerView)
horizontalRecyclerView.addOnScrollListener(this);
}
}
break;
case RecyclerView.SCROLL_STATE_SETTLING:
//TODO fix out-of-sync scrolling issues, probably here
case RecyclerView.SCROLL_STATE_DRAGGING:
if (masterRecyclerView == null) {
masterRecyclerView = recyclerView;
final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
if (horizontalRecyclerView != recyclerView)
horizontalRecyclerView.removeOnScrollListener(this);
}
}
}
}
#Override
public void onScrolled(final RecyclerView recyclerView, final int dx, final int dy) {
super.onScrolled(recyclerView, dx, dy);
mCurX += dx;
final int firstVisibleItemPosition = verticalLinearLayoutManager.findFirstVisibleItemPosition();
final int lastVisibleItemPosition = verticalLinearLayoutManager.findLastVisibleItemPosition();
for (int i = firstVisibleItemPosition; i <= lastVisibleItemPosition; ++i) {
RecyclerView horizontalRecyclerView = (RecyclerView) mainRecyclerView.findViewHolderForAdapterPosition(i).itemView;
if (horizontalRecyclerView != recyclerView)
horizontalRecyclerView.scrollBy(dx, dy);
}
}
};
mainRecyclerView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
RecyclerView horizontalRecyclerView = (RecyclerView) layoutInflater.inflate(R.layout.horizontal_recycler_view, parent, false);
horizontalRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this, LinearLayoutManager.HORIZONTAL, false));
horizontalRecyclerView.addOnScrollListener(masterOnScrollListener);
final ViewHolder horizontalViewHolder = new ViewHolder(horizontalRecyclerView) {
};
horizontalRecyclerView.setAdapter(new Adapter() {
#Override
public ViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
return new ViewHolder(layoutInflater.inflate(R.layout.single_item, parent, false)) {
};
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
((TextView) holder.itemView).setText("horizontalRecyclerView:" + horizontalViewHolder.getAdapterPosition() + "\nitem:" + position);
}
#Override
public int getItemCount() {
return 100;
}
});
return horizontalViewHolder;
}
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
//TODO check why this isn't called for some cases
RecyclerView recyclerView = (RecyclerView) holder.itemView;
recyclerView.removeOnScrollListener(masterOnScrollListener);
//TODO scroll to correct location here. The below code doesn't seem to work at all
recyclerView.scrollToPosition(0);
recyclerView.scrollBy(mCurX,0);
recyclerView.addOnScrollListener(masterOnScrollListener);
recyclerView.getAdapter().notifyDataSetChanged();
}
#Override
public int getItemCount() {
return 40;
}
});
}
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
android:id="#+id/activity_main"
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="lb.com.nestedallscrollingrecyclerviewtest.MainActivity"/>
horizontal_recycler_view.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="100dp"/>
single_item.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"/>
The questions
What is wrong in the code that causes it to be out-of-scrolling sync?
Is it possible I also don't get a hold of all the RecyclerViews that I should?
How come the onBindViewHolder of the vertical RecyclerView doesn't get called when I expect it to?
How do I set the x-coordinate scrolling of a horizontal RecyclerView to be as the others, in onBindViewHolder of the vertical one?
I'm not sure if this could be a problem, but what should I do in case each item in each horizontal RecyclerView could be with a different width than the others ?
A bit late to the party but just putting it here in case anyone else stumbles upon the same issue. Please Note that this solution is written in Kotlin and you might have to convert it to Java if that is your language of choice.
Solution
There are a couple of issues that need to be taken into account.
Synchronise scrolling of horizontal recycler views
Retain offset when scrolling vertical recycler view
Add this code in the Adapter for your vertical recycler view
var horizontalRecyclerViews = mutableListOf<RecyclerView>()
var absoluteOffset: Int? = null //Used to solve issue number 2
// matchOffset is used to synchronise the offset of each horizontal recyclerview.
// It is called when a horizontal recyclerview is scrolled with that recyclerview's
// offset. It is also called when the vertical recycler view is scrolled but without
// an offset value (in which case, it uses the absoluteOffset which is set when
// the horizontal scrolling is stopped)
fun matchOffset(offset: Int? = absoluteOffset) {
offset?.let { offsetValue ->
horizontalRecyclerViews.forEach { recyclerView ->
val currentOffset = recyclerView.computeHorizontalScrollOffset()
if (offsetValue != currentOffset) {
recyclerView.scrollBy(offsetValue-currentOffset, 0)
}
}
}
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
...
...
...
val onTouchListener = object: RecyclerView.OnItemTouchListener {
override fun onTouchEvent(p0: RecyclerView, p1: MotionEvent) {
}
override fun onInterceptTouchEvent(p0: RecyclerView, p1: MotionEvent): Boolean {
if (p1.action == MotionEvent.ACTION_UP) {
// This value is used by the vertical recycler view
absoluteOffset = p0.computeHorizontalScrollOffset()
// Disable the fling scroll to make life easier
return true
}
return false
}
override fun onRequestDisallowInterceptTouchEvent(p0: Boolean) {
}
}
val onScrollListener = object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
val value = recyclerView.computeHorizontalScrollOffset()
matchOffset(value)
}
}
...
...
...
//Clear scroll listeners on each bind to stop them from accumulating
horizontalRecyclerView.clearOnScrollListeners()
//Add touch and scroll listeners to horizontalRecyclerView
horizontalRecyclerView.addOnItemTouchListener(onTouchListener)
horizontalRecyclerView.addOnScrollListener(onScrollListener)
//Add each horizontal recyclerView into the mutableList
horizontalRecyclerViews.add(horizontalRecyclerView)
...
...
...
}
To wrap it up for the resolution of issue number 2, add the following scroll listener to your vertical recycler view
val onScrollListener = object: RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
super.onScrolled(recyclerView, dx, dy)
//Cast the Adapter to access the matchOffset method
(recyclerView.adapter as? Adapter)?.matchOffset()
}
}
verticalRecyclerView.addOnScrollListener(onScrollListener)
I'm trying to figure some way to achieve the next kind of view. At the moment I have tried to create a Listview and just make bigger the selected item. But I cannot make the selected item always be in the middle of my view. So now I'm trying to get this with a numberpicker.
But I didn't find any way to hide the divider bar, and make different the selected item and the rest of the view. The idea is get something like in the bottom image.
I think that the ListView may be more configurable than the NumberPicker.
What you can do is use different row layouts dependind if it is the middle one or the others, so your getView(...) method would look like this:
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 1) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.focused_layout, parent, false);
// Do whatever with this view
} else {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.not_focused_layout, parent, false);
// Do whatever with this view
}
return convertView;
}
This way you can customize both layouts both in XML and code. Yo can change the condition if you want the "special" item any other way.
Following is a number picker with custom display values:
final NumberPicker aNumberPicker = new NumberPicker(context);
List<Integer> ids = getIds();
aNumberPicker.setMaxValue(ids.size()-1);
aNumberPicker.setMinValue(0);
mDisplayedIds = new String[ids.size()];
for (int i = 0; i < ids.size(); i++) {
mDisplayedIds[i] = "Nombre"+String.valueOf(ids.get(i)) ;
}
aNumberPicker.setDisplayedValues(mDisplayedIds);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(50, 50);
RelativeLayout.LayoutParams numPickerParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
numPickerParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
relativeLayout.setLayoutParams(params);
relativeLayout.addView(aNumberPicker, numPickerParams);
Also, you can check out some open source library like this one AndroidPicker
You can implement this using RecyclerView with one Holder for Normal Item and one Holder for Selected Item.
Inside your RecyclerView Adapter
private static int SELECTED_ITEM_POSITION = 2;
private static int NORMAL_ITEM = 1;
private static int SELECTED_ITEM = 2;
#Override
public int getItemViewType(int position)
{
if(position == SELECTED_ITEM_POSITION)
return SELECTED_ITEM;
else
return NORMAL_ITEM;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if(viewType == SELECTED_ITEM)
{
YourSelectedViewHolder selectedViewHolder = (YourSelectedViewHolder)layoutInflater.inflate(R.layout.selected_item_layout, parent, false);
return selectedViewHolder;
}
else //viewType == NORMAL_ITEM
{
YourNormalViewHolder normalViewHolder = (YourNormalViewHolder)layoutInflater.inflate(R.layout.normal_item_layout, parent, false);
return normalViewHolder;
}
}
I wanted to achieve a pretty similar effect on one of my project, where I wanted the middle item of my recycler view to be more prominent.
In my case, that said item is only z-translated to give an impression of focus, but the result is pretty similar to what you're describing.
I'll post my code here, in case it could help you go in the right direction :
//We're on the onCreateView in a fragment here
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//First I find the first visible element
int firstVisiblePosition = mLayoutManager.findFirstVisibleItemPosition();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (firstVisiblePosition != -1) {
int lastVisiblePosition = mLayoutManager.findLastVisibleItemPosition();
int itemHeight = mLayoutManager.getChildAt(0).getMeasuredHeight();
int itemTop = mLayoutManager.getChildAt(0).getTop();
//We use a '+' as itemTop will be negative
int delta = itemHeight + itemTop;
int currentItemToBeFocused = (delta < (itemHeight / 2)) ? 1 : 0;
//Reset the z-translation of other items to 0
for (int i = 0, last = (lastVisiblePosition - firstVisiblePosition); i <= last; ++i) {
if (mLayoutManager.getChildAt(i) != null) {
mLayoutManager.getChildAt(i).setTranslationZ(0);
}
}
//And set the z-translation of the current "centered" item
if (mLayoutManager.getChildAt(currentItemToBeFocused) != null) {
mLayoutManager.getChildAt(currentItemToBeFocused).setTranslationZ(10);
}
}
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});
I have a ScrollView which holds a series of Views. I would like to be able to determine if a view is currently visible (if any part of it is currently displayed by the ScrollView). I would expect the below code to do this, surprisingly it does not:
Rect bounds = new Rect();
view.getDrawingRect(bounds);
Rect scrollBounds = new Rect(scroll.getScrollX(), scroll.getScrollY(),
scroll.getScrollX() + scroll.getWidth(), scroll.getScrollY() + scroll.getHeight());
if(Rect.intersects(scrollBounds, bounds))
{
//is visible
}
This works:
Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (imageView.getLocalVisibleRect(scrollBounds)) {
// Any portion of the imageView, even a single pixel, is within the visible window
} else {
// NONE of the imageView is within the visible window
}
Use View#getHitRect instead of View#getDrawingRect on the view you're testing. You can use View#getDrawingRect on the ScrollView instead of calculating explicitly.
Code from View#getDrawingRect:
public void getDrawingRect(Rect outRect) {
outRect.left = mScrollX;
outRect.top = mScrollY;
outRect.right = mScrollX + (mRight - mLeft);
outRect.bottom = mScrollY + (mBottom - mTop);
}
Code from View#getHitRect:
public void getHitRect(Rect outRect) {
outRect.set(mLeft, mTop, mRight, mBottom);
}
If you want to detect that the view is FULLY visible:
private boolean isViewVisible(View view) {
Rect scrollBounds = new Rect();
mScrollView.getDrawingRect(scrollBounds);
float top = view.getY();
float bottom = top + view.getHeight();
if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true;
} else {
return false;
}
}
This extension help detect view fully visible.
It also work if your View is a child of child of ... of ScrollView (eg: ScrollView -> LinearLayout -> ContraintLayout -> ... -> YourView).
fun ScrollView.isViewVisible(view: View): Boolean {
val scrollBounds = Rect()
this.getDrawingRect(scrollBounds)
var top = 0f
var temp = view
while (temp !is ScrollView){
top += (temp).y
temp = temp.parent as View
}
val bottom = top + view.height
return scrollBounds.top < top && scrollBounds.bottom > bottom
}
Note
1) view.getY() and view.getX() return the x,y value to FIRST PARENT.
2) Here is example about how getDrawingRect will return
Link
My Solution is use NestedScrollView Scroll element:
final Rect scrollBounds = new Rect();
scroller.getHitRect(scrollBounds);
scroller.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (myBtn1 != null) {
if (myBtn1.getLocalVisibleRect(scrollBounds)) {
if (!myBtn1.getLocalVisibleRect(scrollBounds)
|| scrollBounds.height() < myBtn1.getHeight()) {
Log.i(TAG, "BTN APPEAR PARCIALY");
} else {
Log.i(TAG, "BTN APPEAR FULLY!!!");
}
} else {
Log.i(TAG, "No");
}
}
}
});
}
To expand a bit on Bill Mote's answer using getLocalVisibleRect, you may want to check if the view is only partially visible:
Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (!imageView.getLocalVisibleRect(scrollBounds)
|| scrollBounds.height() < imageView.getHeight()) {
// imageView is not within or only partially within the visible window
} else {
// imageView is completely visible
}
public static int getVisiblePercent(View v) {
if (v.isShown()) {
Rect r = new Rect();
v.getGlobalVisibleRect(r);
double sVisible = r.width() * r.height();
double sTotal = v.getWidth() * v.getHeight();
return (int) (100 * sVisible / sTotal);
} else {
return -1;
}
}
I faced the same problem today. While Googling and reading Android reference I found this post and a method I ended up using instead;
public final boolean getLocalVisibleRect (Rect r)
Nice of them not to only providing Rect but also boolean indicating if View visible at all. On negative side this method is undocumented :(
I you want to detect if your View is fully visible, try with this method:
private boolean isViewVisible(View view) {
Rect scrollBounds = new Rect();
mScrollView.getDrawingRect(scrollBounds);
float top = view.getY();
float bottom = top + view.getHeight();
if (scrollBounds.top < top && scrollBounds.bottom > bottom) {
return true; //View is visible.
} else {
return false; //View is NOT visible.
}
}
Strictly speaking you can get the visibility of a view with:
if (myView.getVisibility() == View.VISIBLE) {
//VISIBLE
} else {
//INVISIBLE
}
The posible constant values of the visibility in a View are:
VISIBLE
This view is visible. Use with setVisibility(int) and android:visibility.
INVISIBLE
This view is invisible, but it still takes up space for layout purposes. Use with setVisibility(int) and android:visibility.
GONE
This view is invisible, and it doesn't take any space for layout purposes. Use with setVisibility(int) and android:visibility.
Kotlin way;
An extension for listing scroll view's scroll and get an action if child view visible on screen.
#SuppressLint("ClickableViewAccessibility")
fun View.setChildViewOnScreenListener(view: View, action: () -> Unit) {
val visibleScreen = Rect()
this.setOnTouchListener { _, motionEvent ->
if (motionEvent.action == MotionEvent.ACTION_MOVE) {
this.getDrawingRect(visibleScreen)
if (view.getLocalVisibleRect(visibleScreen)) {
action()
}
}
false
}
}
Use this extension function for any scrollable view
nestedScrollView.setChildViewOnScreenListener(childView) {
action()
}
You can use the FocusAwareScrollView which notifies when view becomes visible :
FocusAwareScrollView focusAwareScrollView = (FocusAwareScrollView) findViewById(R.id.focusAwareScrollView);
if (focusAwareScrollView != null) {
ArrayList<View> viewList = new ArrayList<>();
viewList.add(yourView1);
viewList.add(yourView2);
focusAwareScrollView.registerViewSeenCallBack(viewList, new FocusAwareScrollView.OnViewSeenListener() {
#Override
public void onViewSeen(View v, int percentageScrolled) {
if (v == yourView1) {
// user have seen view1
} else if (v == yourView2) {
// user have seen view2
}
}
});
}
Here is class :
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.widget.NestedScrollView;
import android.util.AttributeSet;
import android.view.View;
import java.util.ArrayList;
import java.util.List;
public class FocusAwareScrollView extends NestedScrollView {
private List<OnScrollViewListener> onScrollViewListeners = new ArrayList<>();
public FocusAwareScrollView(Context context) {
super(context);
}
public FocusAwareScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public FocusAwareScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public interface OnScrollViewListener {
void onScrollChanged(FocusAwareScrollView v, int l, int t, int oldl, int oldt);
}
public interface OnViewSeenListener {
void onViewSeen(View v, int percentageScrolled);
}
public void addOnScrollListener(OnScrollViewListener l) {
onScrollViewListeners.add(l);
}
public void removeOnScrollListener(OnScrollViewListener l) {
onScrollViewListeners.remove(l);
}
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
for (int i = onScrollViewListeners.size() - 1; i >= 0; i--) {
onScrollViewListeners.get(i).onScrollChanged(this, l, t, oldl, oldt);
}
super.onScrollChanged(l, t, oldl, oldt);
}
#Override
public void requestChildFocus(View child, View focused) {
super.requestChildFocus(child, focused);
}
private boolean handleViewSeenEvent(View view, int scrollBoundsBottom, int scrollYOffset,
float minSeenPercentage, OnViewSeenListener onViewSeenListener) {
int loc[] = new int[2];
view.getLocationOnScreen(loc);
int viewBottomPos = loc[1] - scrollYOffset + (int) (minSeenPercentage / 100 * view.getMeasuredHeight());
if (viewBottomPos <= scrollBoundsBottom) {
int scrollViewHeight = this.getChildAt(0).getHeight();
int viewPosition = this.getScrollY() + view.getScrollY() + view.getHeight();
int percentageSeen = (int) ((double) viewPosition / scrollViewHeight * 100);
onViewSeenListener.onViewSeen(view, percentageSeen);
return true;
}
return false;
}
public void registerViewSeenCallBack(final ArrayList<View> views, final OnViewSeenListener onViewSeenListener) {
final boolean[] viewSeen = new boolean[views.size()];
FocusAwareScrollView.this.postDelayed(new Runnable() {
#Override
public void run() {
final Rect scrollBounds = new Rect();
FocusAwareScrollView.this.getHitRect(scrollBounds);
final int loc[] = new int[2];
FocusAwareScrollView.this.getLocationOnScreen(loc);
FocusAwareScrollView.this.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
boolean allViewsSeen = true;
#Override
public void onScrollChange(NestedScrollView v, int x, int y, int oldx, int oldy) {
for (int index = 0; index < views.size(); index++) {
//Change this to adjust criteria
float viewSeenPercent = 1;
if (!viewSeen[index])
viewSeen[index] = handleViewSeenEvent(views.get(index), scrollBounds.bottom, loc[1], viewSeenPercent, onViewSeenListener);
if (!viewSeen[index])
allViewsSeen = false;
}
//Remove this if you want continuous callbacks
if (allViewsSeen)
FocusAwareScrollView.this.setOnScrollChangeListener((NestedScrollView.OnScrollChangeListener) null);
}
});
}
}, 500);
}
}
I ended up implementing a combination of two of the Java answers ( #bill-mote https://stackoverflow.com/a/12428154/3686125 and #denys-vasylenko https://stackoverflow.com/a/25528434/3686125 ) in my project as a set of Kotlin extensions, which support either standard vertial ScrollView or HorizontalScrollView controls.
I just tossed these in a Kotlin file named Extensions.kt, no class, just methods.
I used these to determine which item to snap to when a user stops scrolling in various scrollviews in my project:
fun View.isPartiallyOrFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
val scrollBounds = Rect()
horizontalScrollView.getHitRect(scrollBounds)
return getLocalVisibleRect(scrollBounds)
}
fun View.isPartiallyOrFullyVisible(scrollView: ScrollView) : Boolean {
val scrollBounds = Rect()
scrollView.getHitRect(scrollBounds)
return getLocalVisibleRect(scrollBounds)
}
fun View.isFullyVisible(horizontalScrollView: HorizontalScrollView) : Boolean {
val scrollBounds = Rect()
horizontalScrollView.getDrawingRect(scrollBounds)
val left = x
val right = left + width
return scrollBounds.left < left && scrollBounds.right > right
}
fun View.isFullyVisible(scrollView: ScrollView) : Boolean {
val scrollBounds = Rect()
scrollView.getDrawingRect(scrollBounds)
val top = y
val bottom = top + height
return scrollBounds.top < top && scrollBounds.bottom > bottom
}
fun View.isPartiallyVisible(horizontalScrollView: HorizontalScrollView) : Boolean = isPartiallyOrFullyVisible(horizontalScrollView) && !isFullyVisible(horizontalScrollView)
fun View.isPartiallyVisible(scrollView: ScrollView) : Boolean = isPartiallyOrFullyVisible(scrollView) && !isFullyVisible(scrollView)
Example usage, iterating through scrollview's LinearLayout children and logging outputs:
val linearLayoutChild: LinearLayout = getChildAt(0) as LinearLayout
val scrollView = findViewById(R.id.scroll_view) //Replace with your scrollview control or synthetic accessor
for (i in 0 until linearLayoutChild.childCount) {
with (linearLayoutChild.getChildAt(i)) {
Log.d("ScrollView", "child$i left=$left width=$width isPartiallyOrFullyVisible=${isPartiallyOrFullyVisible(scrollView)} isFullyVisible=${isFullyVisible(scrollView)} isPartiallyVisible=${isPartiallyVisible(scrollView)}")
}
}
My way:
scrollView.viewTreeObserver?.addOnScrollChangedListener {
scrollView.getDrawingRect(Rect())
myViewInsideScrollView.getLocalVisibleRect(Rect())
}
I know its very late. But i have a good solution. Below is the code snippet for getting view visibility percentage in scroll view.
First of all set touch listener on scroll view for getting callback for scroll stop.
#Override
public boolean onTouch(View v, MotionEvent event) {
switch ( event.getAction( ) ) {
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
new Handler().postDelayed(new Runnable() {
#Override
public void run() {
if(mScrollView == null){
mScrollView = (ScrollView) findViewById(R.id.mScrollView);
}
int childCount = scrollViewRootChild.getChildCount();
//Scroll view location on screen
int[] scrollViewLocation = {0,0};
mScrollView.getLocationOnScreen(scrollViewLocation);
//Scroll view height
int scrollViewHeight = mScrollView.getHeight();
for (int i = 0; i < childCount; i++){
View child = scrollViewRootChild.getChildAt(i);
if(child != null && child.getVisibility() == View.VISIBLE){
int[] viewLocation = new int[2];
child.getLocationOnScreen(viewLocation);
int viewHeight = child.getHeight();
getViewVisibilityOnScrollStopped(scrollViewLocation, scrollViewHeight,
viewLocation, viewHeight, (String) child.getTag(), (childCount - (i+1)));
}
}
}
}, 150);
break;
}
return false;
}
In above code snippet, we are getting call backs for scroll view touch events and post a runnable after 150 millis(Not mandatory) after getting the callback for scroll stopped. In that runnable we will get location of scroll view on the screen and scroll view height. Then get the direct child viewgroup instance of scroll view and get the child counts. In my case direct child of scroll view is LinearLayout named scrollViewRootChild. Then iterate all the child views of scrollViewRootChild. In above code snippet you can see I am getting the location of the child on the screen in a integer array named viewLocation, get height of view in variable name viewHeight. Then i called a private method getViewVisibilityOnScrollStopped. You can get the understanding of the internal working of this method by reading documentation.
/**
* getViewVisibilityOnScrollStopped
* #param scrollViewLocation location of scroll view on screen
* #param scrollViewHeight height of scroll view
* #param viewLocation location of view on screen, you can use the method of view claas's getLocationOnScreen method.
* #param viewHeight height of view
* #param tag tag on view
* #param childPending number of views pending for iteration.
*/
void getViewVisibilityOnScrollStopped(int[] scrollViewLocation, int scrollViewHeight, int[] viewLocation, int viewHeight, String tag, int childPending) {
float visiblePercent = 0f;
int viewBottom = viewHeight + viewLocation[1]; //Get the bottom of view.
if(viewLocation[1] >= scrollViewLocation[1]) { //if view's top is inside the scroll view.
visiblePercent = 100;
int scrollBottom = scrollViewHeight + scrollViewLocation[1]; //Get the bottom of scroll view
if (viewBottom >= scrollBottom) { //If view's bottom is outside from scroll view
int visiblePart = scrollBottom - viewLocation[1]; //Find the visible part of view by subtracting view's top from scrollview's bottom
visiblePercent = (float) visiblePart / viewHeight * 100;
}
}else{ //if view's top is outside the scroll view.
if(viewBottom > scrollViewLocation[1]){ //if view's bottom is outside the scroll view
int visiblePart = viewBottom - scrollViewLocation[1]; //Find the visible part of view by subtracting scroll view's top from view's bottom
visiblePercent = (float) visiblePart / viewHeight * 100;
}
}
if(visiblePercent > 0f){
visibleWidgets.add(tag); //List of visible view.
}
if(childPending == 0){
//Do after iterating all children.
}
}
If you feel any improvement in this code please contribute.
Using #Qberticus answer which was to the point but great btw, I compined a bunch of codes to check if whenever a scrollview is called and got scrolled it trigger the #Qberticus answer and you can do whatever you want, in my case I have a social network containing videos so when the view is drawed on the screen I play the video same idea like facebook and Instagram.
Here's the code:
mainscrollview.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {
#Override
public void onScrollChanged() {
//mainscrollview is my scrollview that have inside it a linearlayout containing many child views.
Rect bounds = new Rect();
for(int xx=1;xx<=postslayoutindex;xx++)
{
//postslayoutindex is the index of how many posts are read.
//postslayoutchild is the main layout for the posts.
if(postslayoutchild[xx]!=null){
postslayoutchild[xx].getHitRect(bounds);
Rect scrollBounds = new Rect();
mainscrollview.getDrawingRect(scrollBounds);
if(Rect.intersects(scrollBounds, bounds))
{
vidPreview[xx].startPlaywithoutstoppping();
//I made my own custom video player using textureview and initialized it globally in the class as an array so I can access it from anywhere.
}
else
{
}
}
}
}
});