I want to change the height of all views within a recycler view at once. All items can have different heights (defined by their visible views, so not hard-coding possible).
With the press of a button, the heights of all items are changed to a smaller height. To do that, I'm looping through the recycler views and get every single one using findViewHolderForAdapterPosition(int position). This works fine for the visible items on the screen but not for the ones at the edge of the screen. It seems they are already drawn but not yet visible. As a result, findViewHolderForAdapterPosition will return nil for them. And I can't find a way to access them properly. I could use notifyItemChanged(int position) but this results in a weird animation.
Here's the simplified code I'm using to loop through the views and do the animations:
private var dataList = listOf<Any>() // Content list
fun animate(recycler: RecyclerView, isLarge: Boolean) {
this.isLarge = isLarge
for (it in dataList.indices) {
val v = recycler.findViewHolderForAdapterPosition(it)
if (v is ContentViewHolder) { // check type
if (isLarge) { // Transition isLarge -> small
v.binding.animateSmallToLarge(it) // changes visibility using GONE & VISIBLE
} else { // Transition short -> long
v.binding.animateLargeToSmall(it) // changes visibility using GONE & VISIBLE
}
}
}
}
Now I'm a super rookie on Android/Kotlin, so any hint will help! It seems that the animation that cases the views to reduce its size will make these "not accessible but drawn outside the screen" suddenly visible. How can I properly access them or invalidate them so they get redrawn (NOT using notifyItemChanged)? Any ideas?
Related
I have a recylcerview where the method shown below gets called on each selection of an item. The method only runs sucessfully when I do not scroll the recyclerview, so the firstvisible is 0. The moment I scroll down a bit and select something the first and last visible are set correctly in the method, but as can be seen in the screenshot, for reasons I do not understand the childAt returns null instead of the view I can see on my app screen. In the screenshot for position 7, the first 4-6 returned a child.
Can someone explain to me how this can happen? From my pov, getChildAt() should always return a view in this scenario.
first and last are going to be the adapter positions of the data and not the position as laid out in the layout manager. See LinearLayoutManager#findFirstVisibleItemPosition. The children will always start with zero and increase from there.
That is why it works before your scroll since the child at the zeroth index in the layout manager is also the zeroth item in the adapter.
Here is a discussion about the various positions in RecyclerView.
It looks like you want to make changes to all visible items. Your first and last variables will have the correct start/end adapter positions that correspond to what is visible on the screen. You need the adapter positions to call the various "notify" methods.
So, given the adapter positions, we need a map to the views that are represented on the screen. As an example, the following code loops through every visible view and changes the background color of each view.
LinearLayoutManager lm = (LinearLayoutManager) Recycler.getLayoutManager();
// Get adapter positions for first and last visible items on screen.
int firstVisible = lm.findFirstVisibleItemPosition();
int lastVisible = lm.findLastVisibleItemPosition();
for (int i = firstVisible; i <= lastVisible; i++) {
// Find the view that corresponds to this position in the adapter.
View visibleView = lm.findViewByPosition(i);
visibleView.setBackgroundColor(getResources().getColor(android.R.color.holo_red_light));
}
If you use the child methods of the layout manager, you will need to loop from zero to LayoutManager.getChildCount() - 1 to make the changes. You will see each attached view which, I believe, can exceed the number of visible views.
I'm trying to toggle the background color of a single item in a RecyclerView on click. I do this in the onClick function of a ViewHolder that implements OnClickListener. Here is the onClick implementation:
int position = getLayoutPosition();
if (selected.containsKey(position)) {
view.setBackgroundColor(context.getResources().getColor(R.color.white));
selected.remove(position);
} else {
view.setBackgroundColor(context.getResources().getColor(R.color.highlight));
selected.put(position, view);
}
The issue arises when I test this in a populated RecyclerView. If there are many items, selecting one at the start also highlights other items further in the list that are out of view. The other highlighted views are not added to the map 'selected'. Also, after scrolling away from selected views and returning to them, they sometimes change colors. Why does this happen, and how can I fix this?
That is a side effect of the views being recycled. They are literally being reused. Just make sure when your views are being bound you explicitly set the background color to the default color. That should do it.
The app I'm building has a use case in which, based on some data I create sections in a Fragment. To visualize, a given fragment A has a layout with main container element being ScrollView. There, the scroll view has a toolbar (irrelevant for this question) and then ConstraintLayout (showing all the fragment's content, lets call it masterLayout). What I need to be able to do is to, based on a boolean (let's call it hasSection) remove a view component from masterLayout. Therefore, sometimes fragment A will show masterLayout (which is NOT at position 0 in the container - it is 5th component in the view tree) and sometimes it will not. Now, I tried to simply calling:
((ViewGroup)masterLayout.getParent()).removeView(masterLayout);
which works partially. The problem here is, the view is removed, but the space it occupied is not. Therefore, any other children appearing after that view in the view tree will not be "moved up". I can't leave it like that, as the masterLayout has height = 200dp. As you can see, it will occupy a significant portion of the screen. Therefore, after researching the web, I found the solution to set its visibility:
masterLayout.setVisibility(View.GONE);
which works as intended. The view is removed, all further child views are moved up accordingly so there is no empty and unwanted space. The problem is that when I do transition away from that fragment (lets say there is a clickable view which takes the user to a different fragment) and come back to it again (for example using back button with onBackPressed()), masterLayout that has been set to visibility = View.GONE will not show up anymore.
How can I achieve the functionality of removing/hiding a particular view from a layout, with having the next views beneath it moved up to cover empty space (like View.GONE does) while restoring the original layout when navigating back to described fragment?
Full code of the logic:
if(isTypeA){
new FetchUser(USERID){
#Override
protected void onPostExecute(User user){
mUser = user;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new FetchImages(mUser.getType()){
#Override
protected void onPostExecute(List<Image> result){
if(mImageList.isEmpty() && !result.isEmpty()){
mImageList.addAll(result);
mAdapter = new ItemAdapter(getChildFragmentManager(), mImageList.size(), mImageList);
mItemCardPager.setAdapter(mAdapter);
}else{
if(mMasterLayout != null){
mMasterLayout.setVisibility(View.GONE);
}
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
The Fragment lifecycle can help here.
One approach would be to always restore that masterLayout to visibility View.VISIBLE in the fragment's onResume method.
#Override
public void onResume() {
super.onResume();
masterLayout.setVisibility(View.VISIBLE);
}
I have implemented a horizontal recyclerView with LinearSnapHelper, to implement a UI input that selects a particular configuration. Kinda like the old school number picker/selector or spinner. The item in the center is the selected position.
it works fine and all, but here's the problem. On initial start up, I need to programmatically set the position of the recycler view such that the selected item (the index of which was loaded from disk) is position in the center.
.scrollToPosition() wont work becuase it places the selected item in the begining.
now I know I can do all the math and calculate the x coordinate and manually set it, but thats a lot of redundant work because LinearSnapHelper is already doing this, and I feel like there should be a way to just reuse that logic, but with actually initiating a fling.
I need something like LinearSnapHelper.snapToPosition()
More general solution:
First scroll RecyclerView to make target item visible.
Than, take the object of target View and use SnapHelper to determine
distance for the final snap.
Finally scroll to target position.
NOTE: This works only because programmatically you are scrolling at the exact position & covering the missing distance by exact value using scrollBy instead of doing smooth scrolling
Code snippet:
mRecyclerView.scrollToPosition(selectedPosition);
mRecyclerView.post(() -> {
View view = mLayoutManager.findViewByPosition(selectedPosition);
if (view == null) {
Log.e(WingPickerView.class.getSimpleName(), "Cant find target View for initial Snap");
return;
}
int[] snapDistance = mSnapHelper.calculateDistanceToFinalSnap(mLayoutManager, view);
if (snapDistance[0] != 0 || snapDistance[1] != 0) {
mRecyclerView.scrollBy(snapDistance[0], snapDistance[1]);
}
}
});
Try calling smoothScrollToPosition on the RecyclerView object, and passing the position index (int)
mRecyclerView.smoothScrollToPosition(position);
Worked for me with a LinearLayoutManager and LinearSnapHelper. It animates the initial scroll, but at least snaps the item in position.
This is my first post on the stack, hope it helps :)
I have a recyclerView which I have added padding at the left and right with dummy views in the adapter. So that the first "actual" item can be snapped to.
I couldn't get smoothScrollToPosition(0) to work though for the initial snap. I used the following
recycler.scrollBy(snapHelper.calculateDistanceToFinalSnap(binding.recycler.getLayoutManager(), recycler.getChildAt(1))[0], 0);
Isn't the nicest looking way, but seems to work!
I'm working on an app that uses an expandable listview as its parent, and in each child view there is a horizontal scroll view that is loaded dynamically based on DB information.
All of the buttons in the HSV are compound images added dynamically using an implementation very similar to lazy loading in listview. I was able to add arrows to my HSV, but now I would like to take it one step further and only display arrows when there are enough items to fill the space, such that if only one item is showing then no arrows should be displayed.
Since a list starts in the beginning, I can hide the 'back' arrow by default, but I am unsure where to test whether or not to show the 'forward' arrow.
public Object getGroup(int groupPosition) {
Object o;
o = groups.get(groupPosition).getName();
if(relativeLayout != null && horizontalScrollView != null && forward != null) {
if(horizontalScrollView.getWidth() < relativeLayout.getWidth()) {
forward.setVisibility(View.VISIBLE);
} else {
forward.setVisibility(View.INVISIBLE);
}
}
// }
return o;
}
Originally I used this test in getChildView, where the HorizontalScrollView was populated, but it happened too soon and bot the HSV and relativelayout displayed a width of zero.
Right now I am testing it in getGroup, because it is called multiple times AFTER the childview has started to load, but I think this only works because I have a lot of groups in my expandable list. If I had one group I would still have the same problem.
Is there an ideal place to check whether or not the child layout is larger than the horizontalscrollview?
Update: I set a timer that checks every 1000 ms to see if forward should be turned visible and then exits. Its not pretty, but it works without crashing the app, so I'm happy. Sorry for the bad question!
I am checking the scroll position via the onTouch event:
horizontalScrollView.setOnTouchListener(actualScrollViewTouch);