I'd like to get the exact, pixel position of the ListView scroll.
And no, I am not referring to the first visible position.
Is there a way to achieve this?
Okay, I found a workaround, using the following code:
View c = listview.getChildAt(0);
int scrolly = -c.getTop() + listview.getFirstVisiblePosition() * c.getHeight();
The way it works is it takes the actual offset of the first visible list item and calculates how far it is from the top of the view to determine how much we are "scrolled into" the view, so now that we know that we can calculate the rest using the regular getFirstVisiblePosition method.
Saarraz1's answer will only work if all the rows in the listview are of the same height and there's no header (or it is also the same height as the rows).
Note that once the rows disappear at the top of the screen you don't have access to them, as in you won't be able to keep track of their height. This is why you need to save those heights (or accumulated heights of all). My solution requires keeping a Dictionary of heights per index (it is assumed that when the list is displayed the first time it is scrolled to the top).
private Dictionary<Integer, Integer> listViewItemHeights = new Hashtable<Integer, Integer>();
private int getScroll() {
View c = listView.getChildAt(0); //this is the first visible row
int scrollY = -c.getTop();
listViewItemHeights.put(listView.getFirstVisiblePosition(), c.getHeight());
for (int i = 0; i < listView.getFirstVisiblePosition(); ++i) {
if (listViewItemHeights.get(i) != null) // (this is a sanity check)
scrollY += listViewItemHeights.get(i); //add all heights of the views that are gone
}
return scrollY;
}
Simplest idea I could come up with was to extend ListView and expose the "computeVerticalScrollOffset" which is protected by default, then use "com.your.package.CustomListView" in your xml layouts.
public class CustomListView extends ListView {
public CustomListView(Context context) {
super(context);
}
public CustomListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public int computeVerticalScrollOffset() {
return super.computeVerticalScrollOffset();
}
}
First Declare your int variable for hold the position.
int position = 0;
then add scrollListener to your ListView,
listView.setOnScrollListener(new OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
// TODO Auto-generated method stub
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
position = firstVisibleItem;
}
});
Then after getting new data or any changes in your data that time you need to set the listview current position
listView.setSelection(position);
I have used after setup my adapter , works fine for me..
If anyone else found this in Google while looking for a way to track relative scroll offsets in an OnScrollListener - that is, change in Y since the last call to the listener - here's a Gist showing how to calculate that.
I know I'm late to the party but I felt like sharing my solution to this problem. I have a ListView and I was trying to find how much I have scrolled in order to scroll something else relative to it and cause a parallax effect. Here's my solution:
public abstract class OnScrollPositionChangedListener implements AbsListView.OnScrollListener {
int pos;
int prevIndex;
int prevViewPos;
int prevViewHeight;
#Override
public void onScroll(AbsListView v, int i, int vi, int n) {
try {
View currView = v.getChildAt(0);
int currViewPos = Math.round(currView.getTop());
int diffViewPos = prevViewPos - currViewPos;
int currViewHeight = currView.getHeight();
pos += diffViewPos;
if (i > prevIndex) {
pos += prevViewHeight;
} else if (i < prevIndex) {
pos -= currViewHeight;
}
prevIndex = i;
prevViewPos = currViewPos;
prevViewHeight = currViewHeight;
} catch (Exception e) {
e.printStackTrace();
} finally {
onScrollPositionChanged(pos);
}
}
#Override public void onScrollStateChanged(AbsListView absListView, int i) {}
public abstract void onScrollPositionChanged(int scrollYPosition);
}
I created my own OnScrollListener where the method onScrollPositionChanged will be called every time onScroll gets called. But this method will have access to the calculated value representing the amount that the ListView has been scrolled.
To use this class, you can setOnClickListener to a new OnScrollPositionChangedListener and override the onScrollPositionChanged method.
If you need to use the onScroll method for other stuff then you can override that too but you need to call super.onScroll to get onScrollPositionChanged working correctly.
myListView.setOnScrollListener(
new OnScrollPositionChangedListener() {
#Override
public void onScroll(AbsListView v, int i, int vi, int n) {
super.onScroll(v, i, vi, n);
//Do your onScroll stuff
}
#Override
public void onScrollPositionChanged(int scrollYPosition) {
//Enjoy having access to the amount the ListView has scrolled
}
}
);
in addition to #jaredpetker answer.
ListView is not holding all the items in its scroll, so u need to operate only "visible" part of list. When you scroll down top items are shifted out and pushed as new item views. Thats how convertedView is came from (it's not empty item to fill, it's shifted item that is out of "visible" part of list. So u need to know how many items was before visible part multiply them with ListItemHeight and add headerHeight, thes how you can get real absolute offset in scroll. If u got not header, position 0 will be listItem, so you can simplify absoluteY += pos*listItemHeight;
public class CustomListView extends ListView {
private int listItemHeight = 140;
private int headerHeight = 200;
public CustomListView(Context context) {
super(context);
}
public CustomListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public int computeVerticalScrollOffset() {
final int y = super.computeVerticalScrollOffset();
int absoluteY = y;
int pos = getFirstVisiblePosition();
if(pos > 0){
absoluteY += (pos-1)*listItemHeight+headerHeight;
}
//use absoluteY
return y;
}
I had faced the similar problem, That I wanted to place the Vertical Seekbar at current scrolled value of ListView. So I have my own solution like this.
First Create Class
public abstract class OnScrollPositionChangedListener implements AbsListView.OnScrollListener {
int pos;
public int viewHeight = 0;
public int listHeight = 0;
#Override
public void onScroll(AbsListView v, int i, int vi, int n) {
try {
if(viewHeight==0) {
viewHeight = v.getChildAt(0).getHeight();
listHeight = v.getHeight();
}
pos = viewHeight * i;
} catch (Exception e) {
e.printStackTrace();
} finally {
onScrollPositionChanged(pos);
}
}
#Override public void onScrollStateChanged(AbsListView absListView, int i) {}
public abstract void onScrollPositionChanged(int scrollYPosition);
}
Then use it in Main Activity like this.
public class MainActivity extends AppCompatActivity {
SeekBar seekBar;
ListView listView;
OnScrollPositionChangedListener onScrollPositionChangedListener = new OnScrollPositionChangedListener() {
#Override
public void onScroll(AbsListView v, int i, int vi, int n) {
super.onScroll(v, i, vi, n);
//Do your onScroll stuff
}
#Override
public void onScrollPositionChanged(int scrollYPosition) {
//Enjoy having access to the amount the ListView has scrolled
seekBar.setProgress(scrollYPosition);
}
};
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
seekBar = (SeekBar) findViewById(R.id.mySeekBar);
listView = (ListView) findViewById(R.id.listView);
final String[] values = new String[]{"Android List View",
"Adapter implementation",
"Simple List View In Android",
"Create List View Android",
"Android Example",
};
ArrayAdapter<String> adapter = new ArrayAdapter<String>(this,
android.R.layout.simple_list_item_1, android.R.id.text1, values);
listView.setAdapter(adapter);
listView.setOnScrollListener(onScrollPositionChangedListener);
seekBar.postDelayed(new Runnable() {
#Override
public void run() {
seekBar.setMax((onScrollPositionChangedListener.viewHeight * values.length) - onScrollPositionChangedListener.listHeight);
}
}, 1000);
seekBar.setEnabled(false);
}
}
in App Gradle
compile 'com.h6ah4i.android.widget.verticalseekbar:verticalseekbar:0.7.0'
In XML 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"
xmlns:app="http://schemas.android.com/apk/res-auto"
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"
tools:context="com.centsol.charexamples.MainActivity">
<ListView
android:id="#+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fadeScrollbars="false"
android:scrollbars="none"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true">
</ListView>
<!-- This library requires pair of the VerticalSeekBar and VerticalSeekBarWrapper classes -->
<com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBarWrapper
android:layout_width="wrap_content"
android:layout_alignParentRight="true"
android:layout_height="match_parent">
<com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar
android:id="#+id/mySeekBar"
android:layout_width="0dp"
android:progressDrawable="#drawable/progress"
android:thumb="#drawable/thumb"
android:layout_height="0dp"
android:splitTrack="false"
app:seekBarRotation="CW90" /> <!-- Rotation: CW90 or CW270 -->
</com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBarWrapper>
<View
android:layout_width="1dp"
android:visibility="visible"
android:layout_alignParentRight="true"
android:layout_marginTop="16dp"
android:layout_marginRight="2.5dp"
android:layout_marginBottom="16dp"
android:background="#android:color/black"
android:layout_height="match_parent" />
</RelativeLayout>
background xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#android:color/transparent"/>
</shape>
fill xml
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="#android:color/transparent" />
</shape>
progress xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="#android:id/background"
android:drawable="#drawable/background"/>
<item android:id="#android:id/progress">
<clip android:drawable="#drawable/fill" />
</item>
</layer-list>
thumb xml
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval" >
<solid android:color="#android:color/holo_red_dark" />
<size
android:height="5dp"
android:width="5dp" />
</shape>
Related
Currently I'm at the end of my ideas on following issue with LinearLayoutManagers and RecyclerViews on Android:
What scenario I wanted to achieve
A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling. The items being fullscreen sized making them as big as the recyclerview itself. When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
(I'm using support revision 25.1.0)
code snippets
The Pager-class itself
public class VelocityPager extends RecyclerView {
private int mCurrentItem = 0;
#NonNull
private LinearLayoutManager mLayoutManager;
#Nullable
private OnPageChangeListener mOnPageChangeListener = null;
#NonNull
private Rect mViewRect = new Rect();
#NonNull
private OnScrollListener mOnScrollListener = new OnScrollListener() {
private int mLastItem = 0;
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (mOnPageChangeListener == null) return;
mCurrentItem = mLayoutManager.findFirstVisibleItemPosition();
final View view = mLayoutManager.findViewByPosition(mCurrentItem);
view.getLocalVisibleRect(mViewRect);
final float offset = (float) mViewRect.left / ((View) view.getParent()).getWidth();
mOnPageChangeListener.onPageScrolled(mCurrentItem, offset, 0);
if (mCurrentItem != mLastItem) {
mOnPageChangeListener.onPageSelected(mCurrentItem);
mLastItem = mCurrentItem;
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if (mOnPageChangeListener == null) return;
mOnPageChangeListener.onPageScrollStateChanged(newState);
}
};
public VelocityPager(#NonNull Context context) {
this(context, null);
}
public VelocityPager(#NonNull Context context, #Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public VelocityPager(#NonNull Context context, #Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mLayoutManager = createLayoutManager();
init();
}
#NonNull
private LinearLayoutManager createLayoutManager() {
return new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false);
}
#Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
addOnScrollListener(mOnScrollListener);
}
#Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeOnScrollListener(mOnScrollListener);
}
#Override
public void onScrollStateChanged(int state) {
// If you tap on the phone while the RecyclerView is scrolling it will stop in the middle.
// This code fixes this. This code is not strictly necessary but it improves the behaviour.
if (state == SCROLL_STATE_IDLE) {
LinearLayoutManager linearLayoutManager = (LinearLayoutManager) getLayoutManager();
int screenWidth = Resources.getSystem().getDisplayMetrics().widthPixels;
// views on the screen
int lastVisibleItemPosition = linearLayoutManager.findLastVisibleItemPosition();
View lastView = linearLayoutManager.findViewByPosition(lastVisibleItemPosition);
int firstVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition();
View firstView = linearLayoutManager.findViewByPosition(firstVisibleItemPosition);
// distance we need to scroll
int leftMargin = (screenWidth - lastView.getWidth()) / 2;
int rightMargin = (screenWidth - firstView.getWidth()) / 2 + firstView.getWidth();
int leftEdge = lastView.getLeft();
int rightEdge = firstView.getRight();
int scrollDistanceLeft = leftEdge - leftMargin;
int scrollDistanceRight = rightMargin - rightEdge;
if (leftEdge > screenWidth / 2) {
smoothScrollBy(-scrollDistanceRight, 0);
} else if (rightEdge < screenWidth / 2) {
smoothScrollBy(scrollDistanceLeft, 0);
}
}
}
private void init() {
setLayoutManager(mLayoutManager);
setItemAnimator(new DefaultItemAnimator());
setHasFixedSize(true);
}
public void setCurrentItem(int index, boolean smoothScroll) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageSelected(index);
}
if (smoothScroll) smoothScrollToPosition(index);
if (!smoothScroll) scrollToPosition(index);
}
public int getCurrentItem() {
return mCurrentItem;
}
public void setOnPageChangeListener(#Nullable OnPageChangeListener onPageChangeListener) {
mOnPageChangeListener = onPageChangeListener;
}
public interface OnPageChangeListener {
/**
* This method will be invoked when the current page is scrolled, either as part
* of a programmatically initiated smooth scroll or a user initiated touch scroll.
*
* #param position Position index of the first page currently being displayed.
* Page position+1 will be visible if positionOffset is nonzero.
* #param positionOffset Value from [0, 1) indicating the offset from the page at position.
* #param positionOffsetPixels Value in pixels indicating the offset from position.
*/
void onPageScrolled(int position, float positionOffset, int positionOffsetPixels);
/**
* This method will be invoked when a new page becomes selected. Animation is not
* necessarily complete.
*
* #param position Position index of the new selected page.
*/
void onPageSelected(int position);
/**
* Called when the scroll state changes. Useful for discovering when the user
* begins dragging, when the pager is automatically settling to the current page,
* or when it is fully stopped/idle.
*
* #param state The new scroll state.
* #see VelocityPager#SCROLL_STATE_IDLE
* #see VelocityPager#SCROLL_STATE_DRAGGING
* #see VelocityPager#SCROLL_STATE_SETTLING
*/
void onPageScrollStateChanged(int state);
}
}
The item's xml layout
(Note: the root view has to be clickable for other purposes inside the app)
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true">
<LinearLayout
android:id="#+id/icon_container_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:layout_gravity="top|end"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:alpha="0"
android:background="#drawable/info_background"
android:orientation="horizontal"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="#+id/delete"
style="#style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/desc_delete"
android:padding="12dp"
android:src="#drawable/ic_delete_white_24dp"
android:tint="#color/icons" />
</LinearLayout>
<LinearLayout
android:id="#+id/icon_container_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginBottom="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:alpha="0"
android:background="#drawable/info_background"
android:orientation="vertical"
android:padding="4dp"
tools:alpha="1">
<ImageView
android:id="#+id/size"
style="#style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/desc_size"
android:padding="12dp"
android:src="#drawable/ic_straighten_white_24dp"
android:tint="#color/icons" />
<ImageView
android:id="#+id/palette"
style="#style/SelectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:contentDescription="#string/desc_palette"
android:padding="12dp"
android:src="#drawable/ic_palette_white_24dp"
android:tint="#color/icons" />
</LinearLayout>
</RelativeLayout>
The xml layout with the pager itself
(Quite nested? Might be a cause of the problem? I don't know... )
<?xml version="1.0" encoding="utf-8"?>
<android.support.v4.widget.DrawerLayout 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:id="#+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:openDrawer="end">
<SwipeRefreshLayout
android:id="#+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.CoordinatorLayout
android:id="#+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<FrameLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.my.example.OptionalViewPager
android:id="#+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="horizontal"
app:layout_behavior="com.my.example.MoveUpBehavior" />
<android.support.v7.widget.Toolbar
android:id="#+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="#android:color/transparent"
android:clickable="false"
android:fitsSystemWindows="false"
app:contentInsetLeft="0dp"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp"
app:layout_collapseMode="pin"
app:navigationIcon="#drawable/ic_menu_white_24dp" />
</android.support.design.widget.CoordinatorLayout>
</SwipeRefreshLayout>
<include layout="#layout/layout_drawer" />
</android.support.v4.widget.DrawerLayout>
part of my adapter that is relevant for ViewHolders
#Override
public int getItemCount() {
return dataset.size();
}
#Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
Log.v("Adapter", "CreateViewHolder");
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
final View rootView = layoutInflater.inflate(R.layout.page, parent, false);
return new MyViewHolder(rootView);
}
#Override
public void onBindViewHolder(MyViewHolder page, int position) {
Log.v("Adapter", String.format("BindViewHolder(%d)", position));
final ViewData viewData = dataset.get(position);
page.bind(viewData);
listener.onViewAdded(position, viewData.getData());
}
#Override
public void onViewRecycled(MyViewHolder page) {
if (page.getData() == null) return;
listener.onViewRemoved(page.getData().id);
}
#Override
public int getItemViewType(int position) {
return 0;
}
The ViewHolder
public class MyViewHolder extends RecyclerView.ViewHolder implements MyListener {
#BindView(R.id.info_container)
ViewGroup mInfoContainer;
#BindView(R.id.icon_container_top)
ViewGroup mIconContainerTop;
#BindView(R.id.icon_container_bottom)
ViewGroup mIconContainerBottom;
#BindView(R.id.info_rows)
ViewGroup mInfoRows;
#BindView(R.id.loading)
View mIcLoading;
#BindView(R.id.sync_status)
View mIcSyncStatus;
#BindView(R.id.delete)
View mIcDelete;
#BindView(R.id.ic_fav)
View mIcFavorite;
#BindView(R.id.size)
View mIcSize;
#BindView(R.id.palette)
View mIcPalette;
#BindView(R.id.name)
TextView mName;
#BindView(R.id.length)
TextView mLength;
#BindView(R.id.threads)
TextView mThreads;
#BindView(R.id.price)
TextView mPrice;
#Nullable
private MyModel mModel = null;
#Nullable
private Activity mActivity;
public MyViewHolder(View itemView) {
super(itemView);
ButterKnife.bind(this, itemView);
mActivity= (Activity) itemView.getContext();
if (mActivity!= null) mActivity.addMyListener(this);
}
#OnClick(R.id.delete)
protected void clickDeleteBtn() {
if (mActivity == null || mActivity.getMode() != Mode.EDIT) return;
if (mModel == null) return;
Animations.pop(mIcDelete);
final int modelId = mModel.id;
if (mModel.delete()) {
mActivity.delete(modelId);
}
}
#OnClick(R.id.size)
protected void clickSizeBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_SIZE);
Animations.pop(mIcSize);
}
#OnClick(R.id.palette)
protected void clickPaletteBtn() {
if (mActivity== null) return;
mActivity.setUIMode(Mode.EDIT_LENGTH);
Animations.pop(mIcPalette);
}
private void initModelViews() {
if (mData == null) return;
final Locale locale = Locale.getDefault();
mName.setValue(String.format(locale, "Model#%d", mModel.id));
mLength.setValue(Html.fromHtml(String.format(locale, itemView.getContext().getString(R.string.template_length), mModel.meters)));
}
/**
* set the icon container to be off screen at the beginning
*/
private void prepareViews() {
new ExpectAnim().expect(mIconContainerTop).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
new ExpectAnim().expect(mIconContainerBottom).toBe(outOfScreen(Gravity.END), visible())
.toAnimation()
.setNow();
}
#Nullable
public MyModel getData() {
return mModel;
}
private void enableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableEdit() {
new ExpectAnim()
.expect(mIconContainerBottom)
.toBe(outOfScreen(Gravity.END))
.toAnimation()
.start();
}
private void enableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(atItsOriginalPosition())
.toAnimation()
.start();
}
private void disableInfo() {
new ExpectAnim()
.expect(mInfoContainer)
.toBe(outOfScreen(Gravity.BOTTOM))
.toAnimation()
.start();
}
private void enableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(atItsOriginalPosition(), visible())
.toAnimation()
.start();
}
private void disableDelete() {
if (mIconContainerTop == null) return;
new ExpectAnim()
.expect(mIconContainerTop)
.toBe(outOfScreen(Gravity.END), invisible())
.toAnimation()
.start();
}
public void bind(#NonNull final ViewData viewData) {
mModel = viewData.getData();
prepareViews();
initModelViews();
}
}
So, here's my issue with these!
When intializing the adapter I insert about 15 to 17 items via an observable. This seems to be correct:
but when swiping horizontally the recyclerView's callbacks seem to be totally messed up and produce weird results:
Do you see that the recycler does not try to recycle old viewHolders at all? The image just shows a small portion of the "spamming" that is going on. Sometimes it will create a new viewHolder even more than two times for the same position while I scroll the recycler slowly!
Another side problem is: The listener currently should allow me to pass the bind / recycle events to an underlying game engine which will create destroy entities on the screen. Due the excessive spamming of the events it will currently create those entities also excessively!
I excpected the Recycler to create a new ViewHolder for the first (let's say in my example 17) times and then just reuse the items how it should.
Please help, I'm stuck on this problem for 2 days now and I'm frustrated after searching people with same issues but without luck.
Thank you!
There's obviously a problem with ViewHolder recycling. I'm guessing the animations you're running inside MyViewHolder might prevent RecyclerView from recycling holders properly. Make sure you cancel animations at some point, e.g. in RecyclerView.Adapter#onViewDetachedFromWindow().
After you've fixed this, I suggest you follow #EugenPechanec's suggestion to reduce the amount of custom calculations done in the OnScrollListeners. It's better to rely on support library classes and tweak the behavior a little.
When the fling has stopped or the user stops manually, the recycler should scroll to one item (mimicing a viewPager a bit)
Use the official LinearSnapHelper which snaps center of child view to center of RecyclerView.
Use a GravitySnapHelper library which can also snap to start of or end of RecyclerView, just like Google Play store does.
Both of these solutions are applied similarly:
new LinearSnapHelper().attachToRecyclerView(recyclerView);
A horizontal RecyclerView on which the user can swipe very fast without any limitations on fling.
"Without limitations" translates to "infinite speed" meaning a fling would instantly jump to target position. That's probably not what you want.
After going through SnapHelper source I found out that there is a rule: one inch takes 100 milliseconds to scroll. You can override this behavior.
final SnapHelper snapHelper = new LinearSnapHelper() {
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
snapHelper.attachToRecyclerView(recyclerView);
That's the default speed (where MILLISECONDS_PER_INCH = 100). Experiment and find out what fits your needs, start with "one inch takes 50 ms to scroll" and so on.
I have a ExpandableListView which is inside an LinearLayout as container and set by using CustomAdapter.
For its children, I'm using onGroupClick() method to send an request to specific service and getting result as String, then filling child list of clicked group item.
The Problem is since I can't get the updated height (after service response has set to text view of child view's text view) the linearlayout container height doesn't increase the way it should. And it also creates a scrolling problem.
Though list child item xml is WRAP_CONTENT as below:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="#dimen/rowHeight"
android:background="#color/colorLightYellow">
<TextView
android:id="#+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="#dimen/marginGeneral"
android:layout_toLeftOf="#+id/tvCount"
android:gravity="center_vertical"
android:paddingBottom="#dimen/marginGeneral"
android:paddingTop="#dimen/marginGeneral"
android:text="tvTitle"
android:textColor="#color/colorGray"
android:textSize="#dimen/fontSizeNormal" />
...
</RelativeLayout>
So the code part is little long stay with me:
#Override
public boolean onGroupClick(ExpandableListView parent, View v, final int groupPosition, long id) {
Map<Item, List<ItemDetail>> childList = detailExpandableAdapter.getChildList();
final Item item = detailExpandableAdapter.getGroup(groupPosition);
if (childList.get(item).size() == 0) {
startProgressDialog();
GlobalApplication.getService().getItemDetails(Session.getCurrent().getSessionId(), getItem.item.itemNo, item.name, new ServiceCallback<GetItemDetails>() {
#Override
public void onSuccess(GetItemDetails response) {
stopProgressDialog();
List<ItemDetail> itemDetailList = null;
if (GetItemDetails.isSuccess(response)) {
itemDetailList = response.getItemDetailList();
} else {
itemDetail itemDetail = new ItemDetail();
itemDetail.resultDesc = response.getResult().getResultDesc();
if (StringUtils.isNullOrWhitespace(itemDetail.resultDesc)) {
itemDetail.resultDesc = Result.getGeneralFailResult().getResultDesc();
}
itemDetailList = new ArrayList<ItemDetail>();
itemDetailList.add(itemDetail);
}
if (itemDetailList != null) {
Map<Item, List<ItemDetail>> childList = detailExpandableAdapter.getChildList();
if (childList.containsKey(item)) {
childList.remove(item);
}
childList.put(item, itemDetailList);
detailExpandableAdapter.setChildList(childList);
detailExpandableAdapter.notifyDataSetChanged();
detailExpandableAdapter.notifyDataSetInvalidated();
listViewLastItems.expandGroup(groupPosition);
}
}
#Override
public void onFail() {
stopProgressDialog();
}
});
return false;
}
return false;
}
#Override
public void onGroupExpand(int groupPosition) {
setExpandableListViewHeightStable(listViewLastItems, llListViewItemDetailContainer);
if (lastExpanded != -1 && groupPosition != lastExpanded)
listViewItems.collapseGroup(lastExpanded);
lastExpanded = groupPosition;
}
public void setExpandableListViewHeight(ExpandableListView expandableListView, LinearLayout linearLayoutParent){
try {
ExpandableListAdapter expandableListAdapter = expandableListView.getExpandableListAdapter();
int totalHeight = 0;
int desiredWidth = View.MeasureSpec.makeMeasureSpec(expandableListView.getWidth(), View.MeasureSpec.UNSPECIFIED);
for (int i = 0; i < expandableListAdapter.getGroupCount(); i++) {
View groupItem = expandableListAdapter.getGroupView(i, false, null, expandableListView);
groupItem.measure(desiredWidth, View.MeasureSpec.UNSPECIFIED);
//Logger.debug("recalculateExpandableListViewHeight listItem:"+groupItem.getMeasuredHeight());
totalHeight += groupItem.getMeasuredHeight();
if (expandableListView.isGroupExpanded(i)){
for (int j = 0; j < expandableListAdapter.getChildrenCount(i); j++) {
View listItemChild = expandableListAdapter.getChildView(i, j, false, null, expandableListView);
listItemChild.setLayoutParams(new ViewGroup.LayoutParams(desiredWidth, View.MeasureSpec.UNSPECIFIED));
listItemChild.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
Logger.debug("recalculateExpandableListViewHeight listItemChild:" + listItemChild.getMeasuredHeight());
totalHeight += listItemChild.getMeasuredHeight();
}
}
}
linearLayoutParent.getLayoutParams().height = totalHeight + (expandableListAdapter.getGroupCount() * expandableListView.getDividerHeight());
linearLayoutParent.requestLayout();
} catch (Exception e) {
Logger.printStackTrace(e);
}
}
Update: this is the linear layout I use as container
<LinearLayout
android:id="#+id/llListViewItemContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="#+id/tvItemDueDate"
android:layout_marginTop="#dimen/marginGeneral"
android:orientation="vertical"/>
Update 2: I'm adding ExpandableListView to LinearLayout dynamically.
listViewItems = new ExpandableListView(getContext());
listViewItems.setScrollContainer(false);
listViewItems.setDivider(new ColorDrawable(getResources().getColor(R.color.colorLightGray)));
listViewItems.setDividerHeight(UIHelper.convertDptoPixels(1));
listViewItems.setGroupIndicator(null);
listViewItems.setOnGroupClickListener(this);
listViewItems.setOnGroupExpandListener(this);
listViewItems.setOnGroupCollapseListener(this);
//generate empty child list
Map<Item, List<ItemDetail>> childMap = new HashMap<>();
for (Item item : getItems.getItemList()) {
childMap.put(item, new ArrayList<ItemDetail>());
}
detailExpandableAdapter = new detailExpandableAdapter(getActivity(), getItems.getItemList(), childMap);
listViewItems.setAdapter(detailExpandableAdapterF);
listViewItems.removeAllViews();
listViewItems.addView(listViewLastItems);
UIHelper.setExpandableListViewHeightStable(listViewItems, llListViewDetailContainer);
Use below given custom ExpandableListView class and override onMeasure method.
public class MyExpandableListView extends ExpandableListView {
public MyExpandableListView(Context context) {
super(context);
}
public MyExpandableListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyExpandableListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightMeasureSpec_custom = MeasureSpec.makeMeasureSpec(
Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, heightMeasureSpec_custom);
ViewGroup.LayoutParams params = getLayoutParams();
params.height = getMeasuredHeight();
}
}
And use it like,
<com.app.custom.NonScrollExpandableListView
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Replace com.app.custom with your package name in which you put this custom class.
If possible use NestedScrollView instead of ScrollView, as it supports acting as both a nested scrolling parent and child on both new and old versions of Android. Nested scrolling is enabled by default.
Let me know if this help you or not. Happy Coding!!!
I would suggest you to use HeaderView property of ExpandableListView. AS ExpandableListView is a derived class of ListView so HeaderView property must be there as I believe.
Issues in using ListView inside ScrollView -
Performance - If you going to play with measure property of ListView then it will surely affect recycling of cells.
User Experience - Strange behaviour comes when ListView's parent is another scrollable view.
Better move your LinearLayout stuff inside another view, then inflate that view and either put it in Header or Footer of ListView as per your need.
// Inflated View which is going to be use as Header of view
ViewGroup headerView = (ViewGroup)getLayoutInflater().inflate(R.layout.list_header,expListView,false);
// Add that view to Header
your_expandale_listView.addHeaderView(headerView);
I am using listview inside Viewpager where I need to set ListView height based on child and I need to add new Items when user scroll to last position of Listview. But the problem is when I am setting listview height dynamically its making current listview item visible(or selected). That's why getting (calling method to get data) automatically.
Code is given below:
int index = lvNetwork.getFirstVisiblePosition();
View v = lvNetwork.getChildAt(0);
int top = (v == null) ? 0 : v.getTop();
adapter = new NetworkAdapter(activity, R.layout.network_custom_row, networkDataArrayList);
adapter.notifyDataSetChanged();
lvNetwork.setAdapter(adapter);
Utils.setlistViewHeight(lvNetwork, activity);
lvNetwork.setSelectionFromTop(index, top);
lvNetwork.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
int finalItem = firstVisibleItem + visibleItemCount;
Log.d("dataCalling", "visible " + finalItem);
Log.d("dataCalling", "total " + totalItemCount);
if (finalItem == totalItemCount) {
if (preLast != finalItem) {
preLast = finalItem;
Log.d("dataCalling", String.valueOf(totalItemCount));
Log.d("dataCalling", "Page " + nextid);
progressBar.setVisibility(View.VISIBLE);
getNetworkFeed();
}
}
}
});
setlistviewHeight method inside Utils,
public static void setlistViewHeight(ListView listView, Context context) {
ListAdapter myListAdapter = listView.getAdapter();
if (myListAdapter == null) {
return;
}
int totalHeight = 0;
for (int size = 0; size < myListAdapter.getCount(); size++) {
View listItem = myListAdapter.getView(size, null, listView);
if (listItem instanceof ViewGroup)
listItem.setLayoutParams(new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.WRAP_CONTENT,
RelativeLayout.LayoutParams.WRAP_CONTENT));
WindowManager wm = (WindowManager) context
.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
#SuppressWarnings("deprecation")
int screenWidth = display.getWidth();
int listViewWidth = screenWidth - 65;
int widthSpec = View.MeasureSpec.makeMeasureSpec(listViewWidth,
View.MeasureSpec.AT_MOST);
listItem.measure(widthSpec, 0);
totalHeight += listItem.getMeasuredHeight();
}
ViewGroup.LayoutParams params = listView.getLayoutParams();
params.height = totalHeight
+ (listView.getDividerHeight() * (myListAdapter.getCount()));
listView.setLayoutParams(params);
listView.requestLayout();
}
***This code works good if I do not need to set listview height dynamically.
What should I change here to make it work or any alternative solution to get desire result?
Any help will be appreciated.
But the problem is when I am setting listview height dynamically its making current listview item visible
Setting height dynamically is not the problem. Rather, the problem is you are setting the height of the listview as the maximum possible height of listview by calculating height of each item in the list. So what will happen is all the items of the listview will be populated at once and will remain inflated in the list.(NOTE : No view recycling will happen now)
That's why getting (calling method to get data) automatically
The call is happening because you are setting the height of the listview based on the total number of items in the list. What happens because of this is, all the elements in your listview will be in visible state at any given point of time. Which means your condition
if (finalItem == totalItemCount){}
will always be true because your visibleItemCount will always be totalItemCount which makes your final item always equal to totalItemCount. (you can verify this by debugging your app).
What should I change here to make it work or any alternative solution to get desire result?
The best solution I can think of is setting the height of listview if and only if the total height calcuated by you on the basis of heights of all the items is lesser than the height of the screen. Otherwise, set the height of the listview as MATCH_PARENT.
ViewGroup.LayoutParams params = listView.getLayoutParams();
Display display = getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
int height = size.y;
if(totalHeight > height){
params.height = ViewGroup.LayoutParams.MATCH_PARENT;
}else {
Log.d("", "");
params.height = totalHeight
+ (listView.getDividerHeight() * (myListAdapter.getCount()));
}
So this code will prevent making all the views of the listview to become visible and hence onScroll visibleItemCount you will receive, will the no of items currently visible.
Ankit already explained you what's the problem with your code, let me share an alternate solution with you.
As its no good to use listview when you are already populating its items instead it's better to use scrollview and add items dynamically. Scrollview does not have a scroll listener so we customise it to make one.
MyScrollView.Java
public class MyScrollView extends ScrollView {
public interface OnScrollListener {
void onScrollChanged(int l, int t, int oldl, int oldt);
}
private OnScrollListener onScrollListener;
public OnScrollListener getOnScrollListener() {
return onScrollListener;
}
public void setOnScrollListener(OnScrollListener onScrollListener) {
this.onScrollListener = onScrollListener;
}
public MyScrollView(Context context) {
super(context);
}
public MyScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (onScrollListener != null) {
onScrollListener.onScrollChanged(l, t, oldl, oldt);
}
}
}
We use the scrolllistener in activity like this -
import android.graphics.Rect;
import android.os.AsyncTask;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
public class NewScrollActivity extends AppCompatActivity {
private MyScrollView scrollView;
private LinearLayout container;
private ProgressBar progressBar;
int maxItem = 20;
private View lastItemView;
boolean alreadyExecutingRequest = false;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_new_scroll);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
scrollView = (MyScrollView) findViewById(R.id.scrollView);
scrollView.setOnScrollListener(scrollListener);
container = (LinearLayout) findViewById(R.id.container);
addItemsAsynchronously();
}
private MyScrollView.OnScrollListener scrollListener = new MyScrollView.OnScrollListener() {
#Override
public void onScrollChanged(int l, int t, int oldl, int oldt) {
if (lastItemView != null && !alreadyExecutingRequest) {
Rect scrollBounds = new Rect();
scrollView.getHitRect(scrollBounds);
if (lastItemView.getLocalVisibleRect(scrollBounds)) {
// Any portion of the lastitem view, even a single pixel, is within the visible window
addItemsAsynchronously();
}
}
}
};
private void addItemsAsynchronously() {
new AsyncTask<Void,Void,Void>() {
#Override
protected void onPreExecute() {
super.onPreExecute();
progressBar.setVisibility(View.VISIBLE);
alreadyExecutingRequest = true;
}
#Override
protected Void doInBackground(Void... voids) {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
#Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
progressBar.setVisibility(View.GONE);
addItemsToContainer();
alreadyExecutingRequest = false;
}
}.execute();
}
private void addItemsToContainer() {
int lastAddedItem = container.getChildCount();
for (int i=lastAddedItem;i<maxItem;i++) {
View view = LayoutInflater.from(this).inflate(R.layout.new_item, null);
TextView textView = (TextView) view.findViewById(R.id.textView);
textView.setText("Item - " + i);
container.addView(view);
}
lastItemView = container.getChildAt(container.getChildCount() -1);
maxItem+=10;
}
}
Here what we did is we checked the last item bound with the scrollview bounds, so it the view is visible then we are at the bottom, so add further items.
activity_new_scroll.xml
<?xml version="1.0" encoding="utf-8"?>
<com.sj.textinputlayout.MyScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/scrollView"
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"
tools:context="com.sj.textinputlayout.NewScrollActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="#+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<ProgressBar android:id="#+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_gravity="center_horizontal"/>
</LinearLayout>
</com.sj.textinputlayout.MyScrollView>
new_item.xml
<?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:padding="10dp"
android:layout_height="wrap_content">
<TextView android:id="#+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
I have a recyclerview with a gridlayout. What I want is when the user scrolls to the end of the list (see my bad mockup), there should be an empty space with a height of 50dp, which isn't the same dimensions as my grid.
Note that this space is only visible at the very end end, as I do not want to change the layout. I could make it so that the recycerview has a margin bottom of 50dp, but I do not want to do that.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:fab="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:scrollbars="vertical"
/>
</RelativeLayout>
<RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="50dp"
android:clipToPadding="false"
/>
This is best achieved with an item decoration.
Here's an example that works with a LinearLayoutManager - you'll have to adjust to suit for your Grid layout. What it does is checks each item to see if it's the last one, and if it is it adds the offset to the bottom of it. For a Grid layout, the hard part is figuring out whether your item position is in the last row or not.
// After setting layout manager, adapter, etc...
float offsetPx = getResources().getDimension(R.dimen.bottom_offset_dp);
BottomOffsetDecoration bottomOffsetDecoration = new BottomOffsetDecoration((int) offsetPx);
mRecyclerView.addItemDecoration(bottomOffsetDecoration);
...
static class BottomOffsetDecoration extends RecyclerView.ItemDecoration {
private int mBottomOffset;
public BottomOffsetDecoration(int bottomOffset) {
mBottomOffset = bottomOffset;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int dataSize = state.getItemCount();
int position = parent.getChildAdapterPosition(view);
if (dataSize > 0 && position == dataSize - 1) {
outRect.set(0, 0, 0, mBottomOffset);
} else {
outRect.set(0, 0, 0, 0);
}
}
}
For a GridLayoutManager, inside the getItemOffsets method you could do something similar to this to figure out if it's the last row:
GridLayoutManager grid = (GridLayoutManager)parent.getLayoutManager();
if ((dataSize - position) <= grid.getSpanCount()) {
outRect.set(0, 0, 0, mBottomOffset);
} else {
outRect.set(0, 0, 0, 0);
}
I had the similar issue. After reading all others replies and I found the changes in layout xml for recyclerview worked for my recycler view as expected:
android:paddingBottom="127dp"
android:clipToPadding="false"
android:scrollbarStyle="outsideOverlay"
The complete layout looks like:
<android.support.v7.widget.RecyclerView
android:id="#+id/library_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="160dp"
android:layout_marginEnd="160dp"
tools:listitem="#layout/library_list_item" />
For the effect of before and after see the link at androidblog.us:
Adding Space to End of Android Recylerview
Let me know how it works for you.
David
You can try the below code, remember "I do not test this code"
public class MyRecyclerView extends RecyclerView {
private Context context;
public MyRecyclerView(Context context) {
super(context);
this.context = context;
}
public MyRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public MyRecyclerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.context = context;
}
#Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
View view = (View) getChildAt(getChildCount()-1);
int diff = (view.getBottom()-(getHeight()+getScrollY()+view.getTop()));
if( diff == 0 ){ // if diff is zero, then the bottom has been reached
TextView tv = new TextView(context);
tv.setHeight(dpToPx(50));
addView(tv,getChildCount());//update --> add to last
requestLayout();
}
super.onScrollChanged(l, t, oldl, oldt);
}
public int dpToPx(int dp) {
DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
int px = Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
return px;
}
}
and in layout:
<your_packagae.MyRecyclerView
android:id="#+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
/>
Make a class name with BottomOffsetDecoration
public class BottomOffsetDecoration extends RecyclerView.ItemDecoration {
private int mBottomOffset;
public BottomOffsetDecoration(int bottomOffset) {
mBottomOffset = bottomOffset;
}
#Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int dataSize = state.getItemCount();
int position = parent.getChildAdapterPosition(view);
if (dataSize > 0 && position == dataSize - 1) {
outRect.set(0, 0, 0, mBottomOffset);
} else {
outRect.set(0, 0, 0, 0);
}
}
}
then Add these line after adding adapter and layoutmanager to recyclerview
float offsetPx = getResources().getDimension(R.dimen.bottom_offset_dp);
BottomOffsetDecoration bottomOffsetDecoration = new BottomOffsetDecoration((int) offsetPx);
rv.addItemDecoration(bottomOffsetDecoration);
and for GridLayout Add these lines after assigning recyclerview layout manager
GridLayoutManager grid = (GridLayoutManager)parent.getLayoutManager();
if ((dataSize - position) <= grid.getSpanCount()) {
outRect.set(0, 0, 0, mBottomOffset);
} else {
outRect.set(0, 0, 0, 0);
}
My specific question is: How I can achieve an effect like this: 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.
Thanks.
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.
Sticky List Headers
Sticky Scroll Views
HeaderListView
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>
For those looking for a solution in 2020, I have quickly created a solution extending the Layout Manager from ruslansharipov project (Sticky Header) and combining it whith the RecycleView Adapter from lisawray Groupie project (Expandable RecycleView).
You can see my example here
Result Here
You can reach this effect using SuperSLiM library. It provides you a LayoutManager for RecyclerView with interchangeable linear, grid, and staggered displays of views.
A good demo is located in github repository
It is simply to get such result
app:slm_headerDisplay="inline|sticky"
or
app:slm_headerDisplay="sticky"
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/
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 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>