I have a scenario in which I need to know if a child view of a RecyclerView item is visible on screen.
In this case, each RecyclerView item has a TextView and I need to know if that TextView is fully visible on screen. I've already figured this part out, but now my question is this:
How can I make a call to the adapter from my fragment to let it know that the view is visible on screen? What best practice should I follow for this?
Here is my fragment class method where I get the visible child view:
private void getFirstVisibleChildView() {
int findFirstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();
int findLastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
int [] positions = { findFirstVisibleItemPosition, findLastVisibleItemPosition };
PostAdapter.PostViewHolder viewHolder;
Rect scrollBounds = new Rect();
recyclerView.getDrawingRect(scrollBounds);
int[] location = new int[2];
for (int position : positions) {
RecyclerView.ViewHolder item = recyclerView.findViewHolderForAdapterPosition(position);
if (item instanceof PostAdapter.PostViewHolder) {
viewHolder = (PostAdapter.PostViewHolder) item;
viewHolder.getChildView().getLocationInWindow(location);
if (location[1] < 0 || location[1] > scrollBounds.bottom) {
// Not visible
} else {
// Visible
// How to call the RecyclerView adapter here and be able to manipulate it?
// Custom listener, direct call to ViewHolder, or some other method?
}
}
}
}
Thanks!
You can set addOnScrollListener on the RecyclerView, so it checkes if your specific view is fully shown whenever it's scrolled
And normally create a custom method in your adapter that you want to call within this listener when the above condition is met.
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(#NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (mRecyclerViewTafsir.getLayoutManager() == null) return;
// Here you can check if the particular textView is fully
// appeared on the screen >> You already did this part
mAdapter.callBack(); // call a method in the adapter when the condition is met
}
});
I have two problems caused by the same source.
I'm working on a social media app, I have a profile view containing a CollapsingToolbarLayout for the profile picture and RecyclerView inside a NestedScrollView for the posts feed.
The behaviour I didn't expect is that let's say I have 20 posts in the RecyclerView and the screen can only display 3, the recycler adapter creates 20 view holders and they are all considered as visible.
This causes two problems for me :
1 - The posts may contain videos and I want the video to be stopped if the post is not visible on the screen. I used to do this on my other RecyclerViews.
#Override
public void onViewDetachedFromWindow(#NonNull RecyclerView.ViewHolder holder) {
if (holder instanceof PostViewHolder) {
PostViewHolder postViewHolder = (PostViewHolder) holder;
pauseVideo(postViewHolder.videoPlayer);
}
}
This method is never called because the RecyclerView or adapter or whatever considers all the view holders to be visible on the screen. To make sure my assumption is correct I did a log on onViewAttachedToWindow and if the list contains 20 posts, it gets called 20 times when I add the list to the RecyclerView.
2 - I want posts to be loaded dynamically (load small batches on scroll). This was achieved using this method :
WrapContentLinearLayoutManager llManager = new WrapContentLinearLayoutManager(this, LinearLayoutManager.VERTICAL, false);
recyclerView.setLayoutManager(llManager);
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if(dy > 0) { //check for scroll down
if (llManager.findFirstVisibleItemPosition() + 10 > llManager.getItemCount() && !loadingMorePosts) {
loadingMorePosts = true;
dbListeners.getMoreUserPosts();
}
}
}
});
This doesn't work too because llManager.findFirstVisibleItemPosition() always returns 0.
Am I doing something wrong or is this the expected behaviour from a RecyclerView inside a nested ScrollView?
And is there a solution or a workaround for the second problem because loading all the posts at once is not acceptable.
And Thanks.
You have to handle pagination at scroll listener of nesterscroll view
nestedScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
#Override
public void onScrollChange(NestedScrollView view, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
if (scrollY == (view.getChildAt(0).getMeasuredHeight() - view.getMeasuredHeight())&& !loadingMorePosts) {
loadingMorePosts = true;
dbListeners.getMoreUserPosts();
}
}
});
This question already has answers here:
RecyclerView ItemTouchHelper Buttons on Swipe
(12 answers)
Closed 3 years ago.
I have created RecyclerView which contains CardView in order to show data. I would like to implement iOS style of swiping list elements to show action buttons.
My method which should allow me to show icon after swiping left an RecyclerView item:
public void initializeListeners() {
ItemTouchHelper.SimpleCallback simpleItemTouchCallback = new ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
#Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
return false;
}
#Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
int position = viewHolder.getAdapterPosition();
if (direction == ItemTouchHelper.LEFT) {
Toast.makeText(getView().getContext(),"LEFT",Toast.LENGTH_LONG).show();
}
}
#Override
public void onChildDraw(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
Bitmap icon;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
View itemView = viewHolder.itemView;
float height = (float) itemView.getBottom() - (float) itemView.getTop();
float width = height / 3;
if (dX < 0) {
p.setColor(Color.parseColor("#D32F2F"));
RectF background = new RectF((float) itemView.getRight() + dX/4, (float) itemView.getTop(), (float) itemView.getRight(), (float) itemView.getBottom());
c.drawRect(background, p);
icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_delete_black_24dp);
RectF icon_dest = new RectF((float) itemView.getRight() - 2 * width, (float) itemView.getTop() + width, (float) itemView.getRight() - width, (float) itemView.getBottom() - width);
c.drawBitmap(icon, null, icon_dest, p);
}
}
super.onChildDraw(c, recyclerView, viewHolder, dX/4, dY, actionState, isCurrentlyActive);
}
};
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(simpleItemTouchCallback);
itemTouchHelper.attachToRecyclerView(binding.myPlans);
}
effect of this is:
I would like to make this icon clickable in order to send HTTP request using id of object of clicked possition in RecyclerView (after alertview confirmation)
is it possible? I was trying to replace Bitmap for ImageButton with no success
If you consider Swipeable items in lists, the logic is a bit different for Android and iOS. In Android you don't need to confirm deletion with a click. The fact that user swiped the item is enough of a confirmation.
That's why ItemTouchHelper won't give you a way to attach an OnClickListener.
You have two choices:
You can write your own custom swipe management system (painful).
Agree on Android way of doing that and ask user for confirmation after the swipe.
I wrote a blog post describing the steps needed for implementing this kind of feature.
Add the following class to your project: SwipeRevealLayout.java
Adjust your layout code for your RecyclerView ViewHolder and add the SwipeRevealLayout component as the container for both the top and bottom layer of your RecyclerView Item. For an example of how to set it up: list_item_main.xml
Ensure the bottom layer is the first layout component within the SwipeRevealLayout container.
Make sure to use ‘wrap_content’ or a predefined width for your bottom layer. I tested out using ‘match_parent’ and the top layer did a good magic trick and disappeared.
If you are adding a clickable function on the bottom layer, ensure the top layer has android:clickable=”true” otherwise clicks for the bottom layer components will still trigger when you click on the top layer.
Optional: You can define what edge you want to drag from. By default, it will drag from the left, in the example project I defined it to drag from the right. Specify it with app:dragFromEdge=”{edge to drag from}" when specifying the attributes for the SwipeRevealLayout component.
If you're interested in viewing the full blog post, check it out here: https://android.jlelse.eu/android-recyclerview-swipeable-items-46a3c763498d
The background drawn is a Canvas, Canvas don't allow to implements clicks.
I have found a way to simulate the click on your trash view within the onInterceptTouchEvent of RecyclerView.OnItemTouchListener.
First, in your onBindViewHolder you have to set the tag of your view as it viewHolder : viewHolder.itemView.setTag(viewHolder).
Then :
#Override
public void onInterceptTouchEvent (RecyclerView rv, MotionEvent e) {
View viewSwipedRight = rv.findChildViewUnder(e.getX() - rv.getWidth(), e.getY());
if (viewSwipedRight != null && e.getAction() == MotionEvent.ACTION_DOWN) {
ViewHolder viewHolder = (ViewHolder) viewSwipedRight.getTag();
if (e.getX() >= viewHolder.trashIcon.getX()) {
// Your icon is clicked !
}
}
}
Explanation :
When swiped left, your view is still here, but not on screen because it has moved at the left of the recyclerview, so View viewSwipedRight = rv.findChildViewUnder(e.getX() - rv.getWidth(), e.getY()); will find the view at your Y click at the left of the recyclerView on screen (thanks to e.getX() - rv.getWidth()).
Then you look if the event X matches with your trash icon's X within its own view with e.getX() >= viewHolder.trashIcon.getX() (your trashIcon has to be in your viewHolder).
If both of those conditions match, you have clicked your trash icon.
Iphone App video link
How I can design and develop view which is posted in above video? This is basically the item expansion of recyclerview with animation. I have tried with onItemtouchlistener of recyclerview and also with some custom view with animation, but didn't get the accurate result.
Finally i came accross addonscrolllistener, this give me results but not accurate.
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
if(newState == RecyclerView.FOCUS_UP) {
System.out.println("hello, ia m going up");
}
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
if (dy > 0){
TextView tv = (TextView)recyclerView.findViewById(R.id.title);
//tv.setVisibility(View.VISIBLE);
if (tv.getVisibility()==View.VISIBLE){
System.out.println("yes");
}else {
slideToTop(tv);
}
}
}
});
private void slideToTop(View view){
TranslateAnimation animate = new TranslateAnimation(0,0,0,-view.getHeight());
animate.setDuration(1000);
animate.setFillAfter(false);
view.startAnimation(animate);
view.setVisibility(View.VISIBLE);
}
I think your question is too broad - however, here is some psuedocode:
onScrolled() {
View child = getSecondVisibleChild();
int distanceFromTop = child.getTop();
int distanceAtWhichExpandingShouldOccur = 100;
if(distanceFromTop < distanceAtWhichExpandingShouldOccur ) {
child.setHeight(child.getOriginalHeight() + (distanceFromTop - distanceAtWhichExpandingShouldOccur))
}
}
So you'll notice, the second visible child is the one who's height changes. It changed when it's less than distanceAtWhichExpandingShouldOccur from the top of the window. Its height changes to ensure its bottom remains stationary - therefore its height is increasing at the same pace its top is moving.
Once it's no longer the second visible child (aka, its top is 0), it should be scrolled off as normal and the next child should have its height changed when its top is less than distanceAtWhichExpandingShouldOccur.
This library can be a study case for you: https://github.com/florent37/MaterialLeanBack
I'm looking for a way to scroll a RecyclerView to show the selected item on top.
In a ListView I was able to do that by using scrollTo(x,y) and getting the top of the element that need to be centered.
Something like:
#Override
public void onItemClick(View v, int pos){
mylistView.scrollTo(0, v.getTop());
}
The problem is that the RecyclerView returns an error when using it's scrollTo method saying
RecyclerView does not support scrolling to an absolute position
How can I scroll a RecyclerView to put the selected item at the top of the view?
If you are using the LinearLayoutManager or Staggered GridLayoutManager, they each have a scrollToPositionWithOffset method that takes both the position and also the offset of the start of the item from the start of the RecyclerView, which seems like it would accomplish what you need (setting the offset to 0 should align with the top).
For instance:
//Scroll item 2 to 20 pixels from the top
linearLayoutManager.scrollToPositionWithOffset(2, 20);
If you looking for vertical LinearLayout Manager you can achieve smooth scrolling using a custom LinearSmoothScroller:
import android.content.Context;
import android.graphics.PointF;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.LinearSmoothScroller;
import android.support.v7.widget.RecyclerView;
public class SnappingLinearLayoutManager extends LinearLayoutManager {
public SnappingLinearLayoutManager(Context context, int orientation, boolean reverseLayout) {
super(context, orientation, reverseLayout);
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext());
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
public TopSnappedSmoothScroller(Context context) {
super(context);
}
#Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return SnappingLinearLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}
#Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;
}
}
}
use an instance of the layoutmanager in recycle view and then calling recyclerView.smoothScrollToPosition(pos); will smooth scroll to selected position to top of the recycler view
//Scroll item pos
linearLayoutManager.scrollToPositionWithOffset(pos, 0);
You just need to call recyclerview.scrollToPosition(position). That's fine!
If you want to call it in adapter, just let your adapter has the instance of recyclerview or the activity or fragment which contains recyclerview,than implements the method getRecyclerview() in them.
I hope it can help you.
If you want to scroll automatic without show scroll motion then you need to write following code:
mRecyclerView.getLayoutManager().scrollToPosition(position);
If you want to display scroll motion then you need to add following code.
=>Step 1: You need to declare SmoothScroller.
RecyclerView.SmoothScroller smoothScroller = new
LinearSmoothScroller(this.getApplicationContext()) {
#Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
=>step 2: You need to add this code any event you want to perform scroll to specific position.
=>First you need to set target position to SmoothScroller.
smoothScroller.setTargetPosition(position);
=>Then you need to set SmoothScroller to LayoutManager.
mRecyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
just call this method simply:
((LinearLayoutManager)recyclerView.getLayoutManager()).scrollToPositionWithOffset(yourItemPosition,0);
instead of:
recyclerView.scrollToPosition(yourItemPosition);
same with speed regulator
public class SmoothScrollLinearLayoutManager extends LinearLayoutManager {
private static final float MILLISECONDS_PER_INCH = 110f;
private Context mContext;
public SmoothScrollLinearLayoutManager(Context context,int orientation, boolean reverseLayout) {
super(context,orientation,reverseLayout);
mContext = context;
}
#Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
RecyclerView.SmoothScroller smoothScroller = new TopSnappedSmoothScroller(recyclerView.getContext()){
//This controls the direction in which smoothScroll looks for your view
#Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return new PointF(0, 1);
}
//This returns the milliseconds it takes to scroll one pixel.
#Override
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;
}
};
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
private class TopSnappedSmoothScroller extends LinearSmoothScroller {
public TopSnappedSmoothScroller(Context context) {
super(context);
}
#Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return SmoothScrollLinearLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}
#Override
protected int getVerticalSnapPreference() {
return SNAP_TO_START;
}
}
}
Try what worked for me cool!
Create a variable private static int displayedposition = 0;
Now for the position of your RecyclerView in your Activity.
myRecyclerView.setOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
LinearLayoutManager llm = (LinearLayoutManager) myRecyclerView.getLayoutManager();
displayedposition = llm.findFirstVisibleItemPosition();
}
});
Place this statement where you want it to place the former site displayed in your view .
LinearLayoutManager llm = (LinearLayoutManager) mRecyclerView.getLayoutManager();
llm.scrollToPositionWithOffset(displayedposition , youList.size());
Well that's it , it worked fine for me \o/
what i did to restore the scroll position after refreshing the RecyclerView on button clicked:
if (linearLayoutManager != null) {
index = linearLayoutManager.findFirstVisibleItemPosition();
View v = linearLayoutManager.getChildAt(0);
top = (v == null) ? 0 : (v.getTop() - linearLayoutManager.getPaddingTop());
Log.d("TAG", "visible position " + " " + index);
}
else{
index = 0;
}
linearLayoutManager = new LinearLayoutManager(getApplicationContext());
linearLayoutManager.scrollToPositionWithOffset(index, top);
getting the offset of the first visible item from the top before creating the linearLayoutManager object and after instantiating it the scrollToPositionWithOffset of the LinearLayoutManager object was called.
I don't know why I didn't find the best answer but its really simple.
recyclerView.smoothScrollToPosition(position);
No errors
Creates Animations
What i may add here is how to make it work together with DiffUtil and ListAdapter
You may note that calling recyclerView.scrollToPosition(pos) or (recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, offset) wouldn't work if called straight after adapter.submitList. It is because the differ looks for changes in a background thread and then asynchronously notifies adapter about changes. On a SO i have seen several wrong answers with unnecessary delays & etc to solve this.
To handle the situation properly the submitList has a callback which is invoked when changes have been applied.
So the proper kotlin implementations in this case are:
//memorise target item here and a scroll offset if needed
adapter.submitList(items) {
val pos = /* here you may find a new position of the item or just use just a static position. It depends on your case */
recyclerView.scrollToPosition(pos)
}
//or
adapter.submitList(items) { recyclerView.smoothScrollToPosition(pos) }
//or etc
adapter.submitList(items) { (recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, offset) }
Introduction
None of the answers explain how to show last item(s) at the top. So, the answers work only for items that still have enough items above or below them to fill the remaining RecyclerView. For instance, if there are 59 elements and a 56-th element is selected it should be at the top as in the picture below:
So, let's see how to implement this in the next paragraph.
Solution
We could handle those cases by using linearLayoutManager.scrollToPositionWithOffset(pos, 0) and additional logic in the Adapter of RecyclerView - by adding a custom margin below the last item (if the last item is not visible then it means there's enough space fill the RecyclerView). The custom margin could be a difference between the root view height and the item height. So, your Adapter for RecyclerView would look as follows:
...
#Override
public void onBindViewHolder(ViewHolder holder, final int position) {
...
int bottomHeight = 0;
int itemHeight = holder.itemView.getMeasuredHeight();
// if it's the last item then add a bottom margin that is enough to bring it to the top
if (position == mDataSet.length - 1) {
bottomHeight = Math.max(0, mRootView.getMeasuredHeight() - itemHeight);
}
RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)holder.itemView.getLayoutParams();
params.setMargins(0, 0, params.rightMargin, bottomHeight);
holder.itemView.setLayoutParams(params);
...
}
...
If your LayoutManager is LinearLayoutManager you can use scrollToPositionWithOffset(position,0); on it and it will make your item the first visible item in the list. Otherwise, you can use smoothScrollToPosition on the RecyclerView directly.
I ended up using the below code.
RecyclerView.LayoutManager layoutManager = mainList.getLayoutManager();
if (layoutManager instanceof LinearLayoutManager) {
// Scroll to item and make it the first visible item of the list.
((LinearLayoutManager) layoutManager).scrollToPositionWithOffset(position, 0);
} else {
mainList.smoothScrollToPosition(position);
}
scroll at particular position
and this helped me alot.
by click listener you can get the position in your adapter
layoutmanager.scrollToPosition(int position);
In my case my RecyclerView have a padding top like this
<android.support.v7.widget.RecyclerView
...
android:paddingTop="100dp"
android:clipToPadding="false"
/>
Then for scroll a item to top, I need to
recyclerViewLinearLayoutManager.scrollToPositionWithOffset(position, -yourRecyclerView.getPaddingTop());
please note that if scrollToPosition not work notice that your RecyclerView was inside a NestedScrollView; refer to this post
This is pretty simple
recyclerView.scrollToPosition(position)
If you've Recycler view inside nestedscrollview :
val y = recyclerview.getChildAt(0).y
recyclerview.smoothScrollTo(0, y.toInt())
If your Recycler view is not inside nestedscrollview :
recyclerview.smoothScrollToPosition(index)
or
recyclerview.layoutManager?.smoothScrollToPosition(recyclerview, null ,index)
I use the code below to smooth-scroll an item (thisView) to the top.
It works also for GridLayoutManager with views of different heights:
View firstView = mRecyclerView.getChildAt(0);
int toY = firstView.getTop();
int firstPosition = mRecyclerView.getChildAdapterPosition(firstView);
View thisView = mRecyclerView.getChildAt(thisPosition - firstPosition);
int fromY = thisView.getTop();
mRecyclerView.smoothScrollBy(0, fromY - toY);
Seems to work good enough for a quick solution.