Detect a view is partially visible inside a RecyclerView.ViewHolder - android

Is there a good way to detect or even better, get notified, when a View of interest inside the ViewHolder has moved outside of the window bounds (was scrolled off or partially scrolled off)?
I'm thinking that one option is to set a scroll listener on the RecyclerView and check my LayoutManager for findFirstCompletelyVisibleItemPosition() etc, and calculate which views are no longer visible. Something like:
private RecyclerView.OnScrollListener mScrollListener = new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
calculateVisibility();
}
};
where calculateVisibility() would run on every scroll event, but this will only give me visibility of my ViewHolder positions, not actual ViewHolders or individual views inside those view holders. Which means I need to then lookup from my layout manager the actualy layout for the position, then measure individual views inside the view holder. Something like:
private void calculateVisibility() {
if (!isAdded() || !getUserVisibleHint() || mAdapter == null) {
return;
}
if (mLayoutManager.findFirstCompletelyVisibleItemPosition() == -1
&& mLayoutManager.findLastCompletelyVisibleItemPosition() == -1
&& mLayoutManager.findFirstVisibleItemPosition() == -1
&& mLayoutManager.findLastVisibleItemPosition() == -1) {
return;
}
int firstPartiallyVisiblePosition = mLayoutManager.findFirstCompletelyVisibleItemPosition();
int lastPartiallyVisiblePosition = mLayoutManager.findFirstCompletelyVisibleItemPosition();
View v = mLayoutManager.findViewByPosition(firstPartiallyVisiblePosition);
for (v instanceof MySpecialView) {
boolean visible = isViewVisible(v);
// do stuff based on visibility
}
}
This feels like it's going to be very inefficient.
Methods I could find inside the adapter seem to be more related to recycling and detaching, which won't happen if you simply scroll the view of the screen.
Maybe someone has done something similar?

In case this is useful to anyone, this is what I ended up doing:
private void calculateVisibility() {
if (!isAdded() || !getUserVisibleHint() || mAdapter == null
|| mAdapter.getItemCount() == 0) {
return;
}
if (mLayoutManager.findFirstCompletelyVisibleItemPosition() == -1
&& mLayoutManager.findLastCompletelyVisibleItemPosition() == -1
&& mLayoutManager.findFirstVisibleItemPosition() == -1
&& mLayoutManager.findLastVisibleItemPosition() == -1) {
return;
}
int findFirstVisibleItemPosition = mLayoutManager.findFirstVisibleItemPosition();
int findLastVisibleItemPosition = mLayoutManager.findLastVisibleItemPosition();
int [] positions = {findFirstVisibleItemPosition, findLastVisibleItemPosition};
ViewHolder viewHolder;
Rect scrollBounds = new Rect();
mRecyclerView.getDrawingRect(scrollBounds);
int location[] = new int[2];
for (int position : positions) {
viewHolder = mRecyclerView.getChildViewHolder(
mLayoutManager.findViewByPosition(position));
viewHolder.getMySpecialView().getLocationInWindow(location);
if (location[1] < 0 || location[1] > scrollBounds.bottom) {
// do stuff
}
}
}

Related

Implementing auto fling at an interval on a Recycler View in android

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())
}
}

Modify selected item in Numberpicker Android

I'm trying to figure some way to achieve the next kind of view. At the moment I have tried to create a Listview and just make bigger the selected item. But I cannot make the selected item always be in the middle of my view. So now I'm trying to get this with a numberpicker.
But I didn't find any way to hide the divider bar, and make different the selected item and the rest of the view. The idea is get something like in the bottom image.
I think that the ListView may be more configurable than the NumberPicker.
What you can do is use different row layouts dependind if it is the middle one or the others, so your getView(...) method would look like this:
public View getView(int position, View convertView, ViewGroup parent) {
if (position == 1) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.focused_layout, parent, false);
// Do whatever with this view
} else {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.not_focused_layout, parent, false);
// Do whatever with this view
}
return convertView;
}
This way you can customize both layouts both in XML and code. Yo can change the condition if you want the "special" item any other way.
Following is a number picker with custom display values:
final NumberPicker aNumberPicker = new NumberPicker(context);
List<Integer> ids = getIds();
aNumberPicker.setMaxValue(ids.size()-1);
aNumberPicker.setMinValue(0);
mDisplayedIds = new String[ids.size()];
for (int i = 0; i < ids.size(); i++) {
mDisplayedIds[i] = "Nombre"+String.valueOf(ids.get(i)) ;
}
aNumberPicker.setDisplayedValues(mDisplayedIds);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(50, 50);
RelativeLayout.LayoutParams numPickerParams = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
numPickerParams.addRule(RelativeLayout.CENTER_HORIZONTAL);
relativeLayout.setLayoutParams(params);
relativeLayout.addView(aNumberPicker, numPickerParams);
Also, you can check out some open source library like this one AndroidPicker
You can implement this using RecyclerView with one Holder for Normal Item and one Holder for Selected Item.
Inside your RecyclerView Adapter
private static int SELECTED_ITEM_POSITION = 2;
private static int NORMAL_ITEM = 1;
private static int SELECTED_ITEM = 2;
#Override
public int getItemViewType(int position)
{
if(position == SELECTED_ITEM_POSITION)
return SELECTED_ITEM;
else
return NORMAL_ITEM;
}
#Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
if(viewType == SELECTED_ITEM)
{
YourSelectedViewHolder selectedViewHolder = (YourSelectedViewHolder)layoutInflater.inflate(R.layout.selected_item_layout, parent, false);
return selectedViewHolder;
}
else //viewType == NORMAL_ITEM
{
YourNormalViewHolder normalViewHolder = (YourNormalViewHolder)layoutInflater.inflate(R.layout.normal_item_layout, parent, false);
return normalViewHolder;
}
}
I wanted to achieve a pretty similar effect on one of my project, where I wanted the middle item of my recycler view to be more prominent.
In my case, that said item is only z-translated to give an impression of focus, but the result is pretty similar to what you're describing.
I'll post my code here, in case it could help you go in the right direction :
//We're on the onCreateView in a fragment here
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
//First I find the first visible element
int firstVisiblePosition = mLayoutManager.findFirstVisibleItemPosition();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (firstVisiblePosition != -1) {
int lastVisiblePosition = mLayoutManager.findLastVisibleItemPosition();
int itemHeight = mLayoutManager.getChildAt(0).getMeasuredHeight();
int itemTop = mLayoutManager.getChildAt(0).getTop();
//We use a '+' as itemTop will be negative
int delta = itemHeight + itemTop;
int currentItemToBeFocused = (delta < (itemHeight / 2)) ? 1 : 0;
//Reset the z-translation of other items to 0
for (int i = 0, last = (lastVisiblePosition - firstVisiblePosition); i <= last; ++i) {
if (mLayoutManager.getChildAt(i) != null) {
mLayoutManager.getChildAt(i).setTranslationZ(0);
}
}
//And set the z-translation of the current "centered" item
if (mLayoutManager.getChildAt(currentItemToBeFocused) != null) {
mLayoutManager.getChildAt(currentItemToBeFocused).setTranslationZ(10);
}
}
}
}
#Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
});

Android - Preventing item duplication on RecyclerView

I have a problem regarding RecyclerView duplicating its items on scroll to top.
My RecyclerView populates its View by taking values from an online database on scroll down to a certain threshold. I am well aware of RecyclerView's behavior on reusing the Views, but I don't want that to happen as it'll get confusing due to having Views with different items inside.
I've searched around SO for the solution. Some said that I have to override getItemId like :
#Override
public long getItemId(int id) {
return id;
}
But they don't elaborate more on that.
Tried using setHasStableIds(true); but it's not working. When I scroll down to populate the RecyclerView, then scroll quickly back up, the first item still shows the last item I scrolled to, or any other random item.
I have this in onBindViewHolder :
if(loading)
{
// Do nothing
}
else {
((ObjectViewHolder) holder).progressBar.setVisibility(View.GONE);
((ObjectViewHolder) holder).postListWrapper.setVisibility(View.VISIBLE);
Uri userImageUri = Uri.parse(mDataset.get(position).author_avatar);
...
// The rest of the code
}
Does it have to do with the error I'm getting? The loading is changed to false when the Fragment containing RecyclerView finished getting value from the database.
Here's the RecyclerView onScrollListener :
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
#Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
int firstVisibleItem = manager.findFirstCompletelyVisibleItemPosition();
if(firstVisibleItem < 1 )
{
swipeRefreshLayout.setEnabled(true);
}else
{
swipeRefreshLayout.setEnabled(false);
}
totalItemCount = manager.getItemCount();
lastVisibleItem = manager.findLastVisibleItemPosition();
int visibleThreshold = 2;
if(isLoading == false)
{
if (totalItemCount <= lastVisibleItem + visibleThreshold) {
if(lobiAdapter.getItemCount() > 0)
{
if (lobiAdapter.getItemCount() < 5)
{
setIsLoaded();
}else{
// End has been reached
// Do something
PostListAPI postListAPI = new PostListAPI();
postListAPI.query.user_id = userId;
postListAPI.query.post_count = String.valueOf(counter);
postListAPI.query.flag = "load";
NewsFeedsAPIFunc newsFeedsAPIFunc = new NewsFeedsAPIFunc(BottomLobiFragment.this.getActivity());
newsFeedsAPIFunc.delegate = BottomLobiFragment.this;
setIsLoading();
newsFeedsAPIFunc.execute(postListAPI);
}
}
else {
setIsLoaded();
}
}
}
}
});

Header in listview like instagram disappears

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

Very Weird Behaviour of Listview

I never seen something like that before but from last few days I am experiencing very peculiar behavior of the Listview and until now I am not been able to isolate the issue.
I only paste the code which I think is necessary and later I will tell you my problem.
/* tell adapter that data is done and stop the more loading progress bar*/
public void setDataChanged(boolean type)
{
isLoadingData = false;
notifyDataSetChanged();
}
/* If loading is going on size of the adapter will increase to show the additional progress bar*/
#Override
public int getCount() {
int size = friendsModels.size();
if (isLoadingData) {
size += 1;
}
Log.i("size", String.valueOf(size));//to check size
return size;
}
/* set loading true and page number of the data items*/
public void setLoadingData(boolean isLoadingData, int page) {
this.isLoadingData = isLoadingData;
this.page = page;
notifyDataSetChanged();
}
/* MAX_ITEM number of item returning from rest webservice per page*/
#Override
public int getItemViewType(int position) {
if(isLoadingData && position%MAX_ITEM==0 && position>0)
{
if(position/MAX_ITEM==page+1)
return 1;
}
return 0;
}
#Override
public View getView(final int position, View convertView, ViewGroup parent) {
Log.e("getView", "getView");
EventHolder eventHolder;
int type = getItemViewType(position);
LayoutInflater li = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if(convertView==null){
eventHolder = new EventHolder();
if(type==0)
{
convertView = li.inflate(R.layout.friends_list_items, parent,false);
eventHolder.name = (TextView)convertView.findViewById(R.id.textview_friend_name);
convertView.setTag(eventHolder);
}
else if(type==1)
{
convertView = li.inflate(R.layout.progress_dialog, parent,false);
eventHolder.progress = (RelativeLayout)convertView.findViewById(R.id.progress_layout);
convertView.setTag(eventHolder);
}
}
else
{
eventHolder = (EventHolder) convertView.getTag();
if(type==0)
{
setFriends(eventHolder, position);
}
}
return convertView;
}
Now onScroll method-
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
if(firstVisibleItem + visibleItemCount >= totalItemCount && !loading && totalItemCount%6==0 && totalItemCount>0 && NetworkUtil.isNetworkAvailable(activity))
{
loading = true;
friendsAdapter.setLoadingData(true,pageNo);
ControllerRequests.getPeople(FragmentClass.this, ++pageNo, search.getText().toString());
}
}
#Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
Though everything is working fine when there are like 5 or more items but as the item size decreases let's say 1 or 2 then sometimes the getView method don't get called though log info is showing me getCount = 1 or 2 but getView just don't get called. There is no pattern, I mean sometimes 5 times getView get called it works fine then suddenly not and like that.
This is a strange check:
if(firstVisibleItem + visibleItemCount >= totalItemCount && !loading &&
totalItemCount%6==0 && totalItemCount>0 &&
NetworkUtil.isNetworkAvailable(activity))
I am referring to:
totalItemCount % 6 == 0
So, unless the total number of items will always be a multiple of 6, this check will prevent friendsAdapter.setLoadingData(true,pageNo); from being called. That's why the statement notifyDataSetChanged(); that resides inside setLoadingData(boolean, int) will not be executed whenever totalItemCount % 6 != 0.
Edit:
I also cannot think of a situation where this will be true:
firstVisibleItem + visibleItemCount > totalItemCount
You can go with the following to check if the user has reached the end of the list:
firstVisibleItem + visibleItemCount == totalItemCount

Categories

Resources