I'm trying to detect when a list view is scrolled beyond certain fixed threshold in pixels (half way through the first item). Unfortunately listview's getScrollY() seems to always return 0 instad of the scroll position. Is there any way to get the actual scroll location by pixel?
Here's the code I tried to use but as said it only returns 0.
getListView().setOnScrollListener(new AbsListView.OnScrollListener() {
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
Log.d("scroll", "scroll: " + getListView().getScrollY());
}
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == 0)
Log.d("scroll", "scrolling stopped");
}
});
There is no notion of Y scroll for a ListView in Android simply because the total height of the content is unknown. Only the height of the displayed content is known.
However it is possible to get the current position/Y scroll of a visible item using the following hack:
getListView().getChildAt(0).getTop();
A refactor code of Malachiasz's answer. This function is used for dynamic row's height.
Call onScroll listener
mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (mCardsListView.getChildCount() > 0) {
int scrollY = getScrollY();
}
}
});
getScrollY function
private Dictionary<Integer, Integer> mListViewItemHeights = new Hashtable<Integer, Integer>();
private int getScrollY() {
View child = mCardsListView.getChildAt(0); //this is the first visible row
if (child == null) return 0;
int scrollY = -child.getTop();
mListViewItemHeights.put(mCardsListView.getFirstVisiblePosition(), child.getHeight());
for (int i = 0; i < mCardsListView.getFirstVisiblePosition(); ++i) {
Integer hei = mListViewItemHeights.get(i);
//Manual add hei each row into scrollY
if (hei != null)
scrollY += hei;
}
return scrollY;
}
You need two things to precisely define the scroll position of a listView:
To get current position:
int firstVisiblePosition = listView.getFirstVisiblePosition();
int topEdge=listView.getChildAt(0).getTop(); //This gives how much the top view has been scrolled.
To set the position:
listView.setSelectionFromTop(firstVisiblePosition,0);
// Note the '-' sign...
listView.scrollTo(0,-topEdge);
You can try implementing OnTouchListener and override its onTouch(View v, MotionEvent event) and get the x and y using event.getX() and event.getY(). I had just created a demo for swipe on ListView row that will enable a delete button for deleting a particular item from the ListView. You can check the source code from my github as ListItemSwipe.
Maybe it will be useful for someone. Code of previous answer's will not work when the list is scrolled fast. Because in this case firstVisiblePosition may be change irregular. In my code I do not use an array to store positions
fun setOnTouchScrollListener(lv: AbsListView, onTouchScrollListener: (isDown: Boolean, offset: Int?, isScrollWorking: Boolean) -> Unit) {
var lastItemPosition: Int = -1
var lastItemTop: Int? = null
var lastItemHeight: Int? = null
var lastScrollState: Int? = AbsListView.OnScrollListener.SCROLL_STATE_IDLE
lv.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScroll(view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int) {
val child = lv.getChildAt(0)
var offset: Int? = null
var isDown: Boolean? = if (firstVisibleItem == lastItemPosition || lastItemPosition == -1) null else firstVisibleItem > lastItemPosition
val dividerHeight = if (lv is ListView) lv.dividerHeight else 0
val columnCount = if (lv is GridView) lv.numColumns else 1
if (child != null) {
if (lastItemPosition != -1) {
when (firstVisibleItem - lastItemPosition) {
0 -> {
isDown = if (lastItemTop == child.top) null else lastItemTop!! > child.top
offset = Math.abs(lastItemTop!! - child.top)
}
columnCount -> offset = lastItemHeight!! + lastItemTop!! - child.top + dividerHeight
-columnCount -> offset = child.height + child.top - lastItemTop!! + dividerHeight
}
}
lastItemPosition = firstVisibleItem
lastItemHeight = child.height
lastItemTop = child.top
if (isDown != null && (offset == null || offset != 0)
&& lastScrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
onTouchScrollListener(isDown, offset, true)
}
}
}
override fun onScrollStateChanged(view: AbsListView, scrollState: Int) {
lastScrollState = scrollState
if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
onTouchScrollListener(true, 0, false)
lastItemPosition = -1
lastItemHeight = null
lastItemTop = null
}
}
})
}
Then we can use when a listview is scrolled beyond threshold
private var lastThresholdOffset = 0
private var thresholdHeight = some value
setOnTouchScrollListener(listView) { isDown: Boolean, offset: Int?, isScrollWorking: Boolean ->
val offset = offset ?: if (isDown) 0 else thresholdHeight
lastThresholdOffset = if (isDown) Math.min(thresholdHeight, lastThresholdOffset + offset)
else Math.max(0, lastThresholdOffset - offset)
val result = lastThresholdOffset > thresholdHeight
}
isScrollWorking - can be used for animation
Related
I have a recycler view with the following attributes in the xml file.
NOTE : I AM DISPLAYING ONLY ONE ITEM OF AT A TIME ON THE SCREEN FOR THIS RECYCLER VIEW.
<MyCustomRecyclerView
android:id="#+id/my_rv"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:orientation="horizontal"
android:overScrollMode="never"
android:paddingHorizontal="4dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
And I am using a PagerSnapHelper to move to the position left or right based on the center of the view on the screen.
val snapHelper = PagerSnapHelper()
snapHelper.attachToRecyclerView(this)
It's working fine for a manual scroll action performed.
Now, I want to add an auto scroll as well after a certain interval of time (say 2.5 seconds). I have created a handler and posted a runnable on it with a delay of 2.5 seconds. I am trying to call fling(velocityX, velocityY) of the RecyclerView with a good enough value of velocityX
val scrollHandler = Handler()
val SCROLL_INTERVAL:Long = 2500 //scroll period in ms
val runnable = object : Runnable {
override fun run() {
//velocityX = 7500
fling(7500, 0)
scrollHandler.postDelayed(this, SCROLL_INTERVAL.toLong())
}
}
But the PagerSnaperHelper::findTargetSnapPosition() not returning correct target position because the View actually has not changed on the screen as in case of a manual scroll. It is returning the position of the element which is already visible on the screen.
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
final OrientationHelper orientationHelper = getOrientationHelper(layoutManager);
if (orientationHelper == null) {
return RecyclerView.NO_POSITION;
}
// A child that is exactly in the center is eligible for both before and after
View closestChildBeforeCenter = null;
int distanceBefore = Integer.MIN_VALUE;
View closestChildAfterCenter = null;
int distanceAfter = Integer.MAX_VALUE;
// Find the first view before the center, and the first view after the center
final int childCount = layoutManager.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
if (child == null) {
continue;
}
final int distance = distanceToCenter(layoutManager, child, orientationHelper);
if (distance <= 0 && distance > distanceBefore) {
// Child is before the center and closer then the previous best
distanceBefore = distance;
closestChildBeforeCenter = child;
}
if (distance >= 0 && distance < distanceAfter) {
// Child is after the center and closer then the previous best
distanceAfter = distance;
closestChildAfterCenter = child;
}
}
// Return the position of the first child from the center, in the direction of the fling
final boolean forwardDirection = isForwardFling(layoutManager, velocityX, velocityY);
if (forwardDirection && closestChildAfterCenter != null) {
return layoutManager.getPosition(closestChildAfterCenter);
} else if (!forwardDirection && closestChildBeforeCenter != null) {
return layoutManager.getPosition(closestChildBeforeCenter);
}
// There is no child in the direction of the fling. Either it doesn't exist (start/end of
// the list), or it is not yet attached (very rare case when children are larger then the
// viewport). Extrapolate from the child that is visible to get the position of the view to
// snap to.
View visibleView = forwardDirection ? closestChildBeforeCenter : closestChildAfterCenter;
if (visibleView == null) {
return RecyclerView.NO_POSITION;
}
int visiblePosition = layoutManager.getPosition(visibleView);
int snapToPosition = visiblePosition
+ (isReverseLayout(layoutManager) == forwardDirection ? -1 : +1);
if (snapToPosition < 0 || snapToPosition >= itemCount) {
return RecyclerView.NO_POSITION;
}
return snapToPosition;
}
I would like to know how can I achieve the desired result?
I got a workaround to solve this. Before calling fling(), I called scrollBy(x,y) to scroll the items as if it would have happened during a manual scroll.
val runnable = object : Runnable {
override fun run() {
scrollBy(400,0)
//velocityX = 7500
fling(7500, 0)
scrollHandler.postDelayed(this, SCROLL_INTERVAL.toLong())
}
}
I have a horizontal list which it's using a recycler view and a PagerSnapHelper to simulate a view pager.
I want to swipe scroll one element at the time, but the problem is that i can scroll to the last element with one swipe.
So my problem is that I want to stop the swipe after one element swiped so if someone has any advice will be appreciated.
You could try adding a logic by checking the current position as mentioned in below post:
SnapHelper Item Position
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if(newState == RecyclerView.SCROLL_STATE_IDLE) {
View centerView = snapHelper.findSnapView(mLayoutManager);
int pos = mLayoutManager.getPosition(centerView);
Log.e("Snapped Item Position:",""+pos);
}
}
or Something like this:
LinearSnapHelper snapHelper = new LinearSnapHelper() {
#Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
View centerView = findSnapView(layoutManager);
if (centerView == null)
return RecyclerView.NO_POSITION;
int position = layoutManager.getPosition(centerView);
int targetPosition = -1;
if (layoutManager.canScrollHorizontally()) {
if (velocityX < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
if (layoutManager.canScrollVertically()) {
if (velocityY < 0) {
targetPosition = position - 1;
} else {
targetPosition = position + 1;
}
}
final int firstItem = 0;
final int lastItem = layoutManager.getItemCount() - 1;
targetPosition = Math.min(lastItem, Math.max(targetPosition, firstItem));
return targetPosition;
}
};
snapHelper.attachToRecyclerView(recyclerView);
Ref: https://ogeek.cn/qa/?qa=441913/&show=441914
I have a horizontal Recyclerview inside another horizontal Recyclerview. Now I want to show the scrollbar. But the height of the scrollbar is changing as I scroll. I want to have a fixed height of the scrollbar as I scroll. How can I achieve that? I have a custom LinearLayoutManager. But it is not working properly. Any better solution to this problem?
class CoolLayoutManager(context: Context,orientation: Int,
reverseLayout: Boolean) : LinearLayoutManager(context,orientation,
reverseLayout) {
init {
isSmoothScrollbarEnabled = true
}
override fun computeHorizontalScrollExtent(state: State): Int {
val count = childCount
return if (count > 0) {
SMOOTH_VALUE * 3
} else 0
}
override fun computeHorizontalScrollRange(state: State): Int {
return Math.max((itemCount - 1) * SMOOTH_VALUE, 0)
}
override fun computeHorizontalScrollOffset(state: State): Int {
val count = childCount
if (count <= 0) {
return 0
}
if (findLastCompletelyVisibleItemPosition() == itemCount - 1) {
return Math.max((itemCount - 1) * SMOOTH_VALUE, 0)
}
val widthOfScreen: Int
val firstPos = findFirstVisibleItemPosition()
if (firstPos == RecyclerView.NO_POSITION) {
return 0
}
val view = findViewByPosition(firstPos) ?: return 0
// Top of the view in pixels
val top = getDecoratedTop(view)
val width = getDecoratedMeasuredWidth(view)
widthOfScreen = if (width <= 0) {
0
} else {
Math.abs(SMOOTH_VALUE * top / width)
}
return if (widthOfScreen == 0 && firstPos > 0) {
SMOOTH_VALUE * firstPos - 1
} else SMOOTH_VALUE * firstPos + widthOfScreen
}
companion object {
private const val SMOOTH_VALUE = 100
}
}
Here is the screenshot of the layout:
I'm using Android-ParallaxHeaderViewPager and StaggeredGridView to create layout like pinterest. I getting problem when getScrollY, the result is sometimes return 0 when scroll at central position on the first row gird , Where first row grid with 2 column/items and different height each items.
imaging :
this the code :
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount, int pagePosition) {
if (mViewPager.getCurrentItem() == pagePosition && visibleItemCount > 0) {
int scrollY = getScrollY(view); // sometimes return 0, when scroll at central position on the first row .
}
}
public int getScrollY(AbsListView view) {
View c = view.getChildAt(0);
if (c == null) {
return 0;
}
int firstVisiblePosition = view.getFirstVisiblePosition();
int top = c.getTop();
int headerHeight = 0;
if (firstVisiblePosition >= 1) {
headerHeight = mHeaderHeight;
}
return -top + firstVisiblePosition * c.getHeight() + headerHeight;
}
so how to fix it ?
I have a problem in listview. If I scroll list, all is good. But if I click on checkbox, header disappears. I checked, notifyOfChange() not started. I think this is due to the drawing of view. Who knows how to make the header is drawn in the last instance.
I use the following code:
#Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//the listview has only few children (of course according to the height of each child) who are visible
for(int i=0; i < list.getChildCount(); i++)
{
View child = list.getChildAt(i);
MuscleViewHolder holder = (MuscleViewHolder) child.getTag();
//if the view is the first item at the top we will do some processing
if(i == 0)
{
boolean isAtBottom = child.getHeight() <= holder.headerLayout.getBottom();
int offset = holder.previousTop - child.getTop();
if(!(isAtBottom && offset > 0))
{
holder.previousTop = child.getTop();
holder.headerLayout.offsetTopAndBottom(offset);
holder.headerLayout.invalidate();
}
} //if the view is not the first item it "may" need some correction because of view re-use
else if (holder.headerLayout.getTop() != 0)
{
int offset = -1 * holder.headerLayout.getTop();
holder.headerLayout.offsetTopAndBottom(offset);
holder.previousTop = 0;
holder.headerLayout.invalidate();
}
}
}
The problem is solved. I replace the code to the following
if(!(isAtBottom && offset > 0)) {
holder.setPreviousTop(child.getTop());
if(itNotScroll){
holder.getHeaderLayout().offsetTopAndBottom(-holder.getPreviousTop());
itNotScroll = false;
} else {
holder.getHeaderLayout().offsetTopAndBottom(offset);
}
holder.getHeaderLayout().invalidate();
}
And yet, I did so, so that you can open only one element in expaned listview