I know this question sounds similar to others on StackOverflow but this is specifically about what happens after an orientation change.
I have an Activity with a Toolbar and a RecyclerView. I use a StaggeredGridLayoutManager with a vertical orientation and 3 columns to fill the RecyclerView. The item layout is a LinearLayout containing an ImageView and a TextView.
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/grid_item_margin"
android:orientation="vertical">
<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="2dp"
android:paddingTop="2dp" />
</LinearLayout>
The images are loaded from the web. I know the image sizes before the ViewHolders are bound. So I set the image height in onBindViewHolder to prevent item reordering and load the image via Picasso:
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
ViewGroup.LayoutParams lp = holder.imageView.getLayoutParams();
lp.height = imageInfos.get(position).imageHeight;
holder.imageView.setLayoutParams(lp);
mPicasso.load(url)
.noFade()
.into(holder.imageView); // imageView scaleType=centerCrop
}
Let's say I have 50 items in my adapter. I am in portrait mode and scroll down 20 items. Then I change to landscape mode. I create a new StaggeredGridLayoutManager with 4 columns now, so one more than in portrait mode. I recalculate the image heights to fit the new target width and set the saved state on the layout like so:
ArrayList<ImageInfo> imageInfos = savedInstanceState
.getParcelableArrayList(IMG_INFOS_KEY);
setNewImgTargetDimensions(imageInfos, columns);
mAdapter = new ImageGridAdapter(mPicasso, imageInfos);
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(layoutMgr);
Parcelable sglmState = savedInstanceState
.getParcelable(LAYOUT_MGR_STATE_KEY);
mRecyclerView.getLayoutManager()
.onRestoreInstanceState(sglmState);
The grid automatically scrolls to the stored position which is desired. Now I scroll up. Because of the new image sizes the "space above the scrolled position" cannot be filled by the images entirely. So items get reordered but most of the time there is still a more or less big gap at the top of the grid (see screenshot links at the bottom). It is worst when I fling to the top. So is there a way to prevent the reordering and the gaps in this case?
portrait mode shows the screen when just loaded for the first time.
landscape mode shows the screen when scrolled up after the orientation change. The image on the left is almost transparent because of a custom fade in animation I guess. I removed that from the sample code to make it smaller.
Edit:
I just realized that all items are aligned at the top of the screen after the orientation change. Though that looks nice, I would prefer if the alignment at the very top of the recycler view was right.
So I came up with a workaround. In short, when the grid is scrolled all the way to the top I set the gap strategy to GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS
so that the items are rearranged.
Here is a more detailed description. When onActivityCreated
is called with saved instance state I check if the orientation has changed. If it has, I set a flag. In the onScrollStateChanged
callback I check if one of the first visible item positions is 0. This is what I consider "scrolled to the top". So if the user scrolled to the top and the orientation changed, I call layoutMgr.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS);
and layoutMgr.invalidateSpanAssignments();
(and reset the flag).