preface
This effect was a demand of e-commerce many years ago. At that time, it imitated an app called meow Street (I don't know if it's still there). The effects after implementation are as follows:

Effect analysis
First, let's look at the static state, as shown in the figure:

At this time, the items displayed at the top are expanded relative to other items, with several manifestations: first, the overall height should be higher; Second, highlight without mask; Third, the text content is larger. This has achieved a prominent effect.
Then we observe the sliding state, as shown in the figure:

When we slide up, we can see that the first item starts to collapse, while the second item gradually expands. At the same time, the masking effect is weakened and the text content becomes larger. This produces the effect of sliding and folding.
Moreover, in order to highlight the last item, we need to insert a footer at the end of the list to ensure that the last item can be displayed on the top, as shown in the figure:

Item layout
After the effect analysis, let's see how to implement it.
The first is the layout of items. Here we only focus on the important parts. The code is as follows:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content"> <RelativeLayout android:id="@+id/item_content" android:layout_width="match_parent" android:layout_height="@dimen/scroll_fold_item_height"> <ImageView android:id="@+id/item_img" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="fitXY"/> <ImageView android:id="@+id/item_img_shade" android:layout_width="match_parent" android:layout_height="match_parent" android:src="#000000"/> <LinearLayout android:id="@+id/scale_item_content" android:layout_width="200dp" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:orientation="vertical"> ... </LinearLayout> ... </RelativeLayout> </FrameLayout>
The outermost layer uses FrameLayout, so that when the FrameLayout height becomes smaller, item_content can go beyond the scope of FrameLayout to produce folding effect.
item_ The height of content is fixed. What really changes is the FrameLayout of the outer layer.
scale_item_content is the text content with variable size
The layout is relatively simple. We will talk about how to use these layouts to achieve results later.
In addition, there is a layout of footer, because it is very simple and does not post code.
Adapter
The list is implemented through RecyclerView, so let's implement the Adapter first. The code is also relatively simple. Let's pick the point.
The first is the implementation of two basic methods of the Adapter:
@Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { if(viewType == 0) { View item = LayoutInflater.from(mContext).inflate(R.layout.scroll_fold_list_item, null); return new ItemViewHolder(item); } else{ View bottom = LayoutInflater.from(mContext).inflate(R.layout.scroll_fold_list_footer, null); return new BottomViewHolder(bottom); } } @Override public void onBindViewHolder(ViewHolder holder, int position) { holder.initData(position); } Use here viewType To distinguish between ordinary item and footer(adopt getItemViewType Method). BottomViewHolder and ItemViewHolder Inherit the same class with the following code: abstract class ViewHolder extends RecyclerView.ViewHolder{ View item; public ViewHolder(View itemView) { super(itemView); item = itemView; } abstract void initData(int position); } class BottomViewHolder extends ViewHolder{ public BottomViewHolder(View itemView) { super(itemView); } @Override void initData(int position) { ViewGroup.LayoutParams bottomParams = itemView.getLayoutParams(); if(bottomParams == null){ bottomParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); } bottomParams.height = recyclerView.getHeight() - itemHeight + 10; itemView.setLayoutParams(bottomParams); } } class ItemViewHolder extends ViewHolder{ View content; ImageView image; TextView name; public ItemViewHolder(View itemView) { super(itemView); item = itemView; content = itemView.findViewById(R.id.item_content); image = (ImageView)itemView.findViewById(R.id.item_img); name = (TextView) itemView.findViewById(R.id.item_name); } void initData(int position){ image.setImageResource(IMGS[position]); name.setText(NAMES[position]); ViewGroup.LayoutParams params = item.getLayoutParams(); if(params == null){ params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0); } params.height = itemSmallHeight; content.findViewById(R.id.item_img_shade).setAlpha(ITEM_SHADE_DARK_ALPHA); item.setLayoutParams(params); } }
Let's first look at the BottomViewHolder. Dynamically set the height of the footer to the list height minus itemHeight, plus 10 pixels. This itemHeight is the height of the expanded item, that is, the height of the top item. The reason why 10 pixels are added here is that if the set height is exactly the remaining height, there is a chance of problems when sliding to the bottom quickly. Therefore, the height is slightly larger than the actual display height.
Then look at the ItemViewHolder. It also dynamically sets the height to ItemSmallHeight, which is the height of the contracted item, and sets the mask to the darkest. Note that all of them are initialized to the contraction state, and there is no separate top expansion state. We will explain why later.
Monitor slide
Above, we have completed the adapter class and added it to RecyclerView. However, to achieve the effect, you need to listen to the sliding of RecyclerView and do the corresponding processing. The code is as follows:
list.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { changeItemState(); } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { ... } }); You can see the sliding process( onScrolled)Call in changeItemState()The code of this function is as follows: private void changeItemState(){ int firstVisibleIndex = linearLayoutManager.findFirstVisibleItemPosition(); ViewGroup first = (ViewGroup) linearLayoutManager.findViewByPosition(firstVisibleIndex); int firstVisibleOffset = -first.getTop(); int changeheight = (int) (firstVisibleOffset * (ScrollFoldAdapter.ITEM_CONTENT_TEXT_SCALE - 1)); // Reduce the height of the first item currently displayed. if (first == null) { return; } changeItemHeight(first, itemHeight - changeheight); changeItemState(first, ScrollFoldAdapter.ITEM_CONTENT_TEXT_SCALE, ScrollFoldAdapter.ITEM_SHADE_LIGHT_ALPHA); // Increase the height of the second item currently displayed, change the content size, and change the transparency if (firstVisibleIndex + 1 < adapter.getItemCount() - 1) { ViewGroup second = (ViewGroup) linearLayoutManager.findViewByPosition(firstVisibleIndex + 1); changeItemHeight(second, itemSmallHeight + changeheight); float scale = (float) firstVisibleOffset / itemSmallHeight * (ScrollFoldAdapter.ITEM_CONTENT_TEXT_SCALE - 1) + 1.0f; float alpha = (ScrollFoldAdapter.ITEM_SHADE_DARK_ALPHA - ScrollFoldAdapter.ITEM_SHADE_LIGHT_ALPHA) * (1 - (float) firstVisibleOffset / itemSmallHeight) + ScrollFoldAdapter.ITEM_SHADE_LIGHT_ALPHA; changeItemState(second, scale, alpha); } /** * Due to the fast sliding, the calculation and status are wrong, so the following is to eliminate this error and calibrate the status. The details are as follows * Change the height above the first item (existing) and below the second item to the contracted height, zoom the content to the minimum, and the transparency is 0.65 */ for (int i = 0; i <= linearLayoutManager.findLastVisibleItemPosition(); i++) { if (i < adapter.getItemCount() - 1 && i != firstVisibleIndex && i != firstVisibleIndex + 1) { ViewGroup item = (ViewGroup) linearLayoutManager.findViewByPosition(i); if(item == null){ continue; } changeItemHeight(item, itemSmallHeight); float scale = 1; float alpha = ScrollFoldAdapter.ITEM_SHADE_DARK_ALPHA; changeItemState(item, scale, alpha); } } }
The overall idea is as follows:
Get the item currently displayed at the top, and calculate the offset of the item relative to the top of the list. This offset is a key parameter. Through this offset, the shrinkage height of the first item and the expansion height of the second item are calculated, and the transparency and text content of the second item mask are calculated.
Here, the other two functions changeItemHeight(view, int) and changeItemState(view, float, float) are called. changeItemHeight(view, int) is used to change the height of the item to expand or collapse; The changeItemState(view, float, float) is used to change the mask transparency and text content size. The two function codes are as follows:
/** * Change the height of an item. * * @param item * @param height */ private void changeItemHeight(View item, int height) { ViewGroup.LayoutParams itemParams = item.getLayoutParams(); itemParams.height = height; item.setLayoutParams(itemParams); } /** * Change the state of an item, including transparency, size, etc * @param item * @param scale * @param alpha */ private void changeItemState(ViewGroup item, float scale, float alpha) { if (item.getChildCount() > 0) { View changeView = item.findViewById(R.id.scale_item_content); changeView.setScaleX(scale); changeView.setScaleY(scale); View shade = item.findViewById(R.id.item_img_shade); shade.setAlpha(alpha); } }
It's easy to change the height. There's no need to explain. To change the transparency of the mask is to change its alpha, and to change the size of the text content is to use the two functions of setscale X and setscale y, which is actually scale_ item_ The content of the layout will become larger / smaller as the whole layout is scaled.
Returning to the changeItemState() function, after changing the first and second items, you can see that other items are set to the shrink state. This is because fast sliding will cause some items to be in the middle state. This step is to correct some problems caused by fast sliding.
As mentioned above, all items are initialized to shrink state. In fact, when RecyclerView is added to the screen, it will definitely slide. So when we enter the page, we do nothing, but the sliding listening function is called. In this way, the top item can be changed into an expanded state through the changeItemState() function, so the initial display state is correct.
Rebound effect
The above is the handling of sliding, but this is not enough. When the sliding stops, it is possible that the first item is half displayed, so the second item is not fully expanded, and the display effect is not good.
Therefore, we also need to achieve a rebound effect. When the sliding stops, the list will be automatically adjusted to the top of an item.
This part is processed in onScrollStateChanged of sliding listening. The code is as follows:
list.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { changeItemState(); } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { if (newState == RecyclerView.SCROLL_STATE_IDLE) { int firstVisibleIndex = linearLayoutManager.findFirstVisibleItemPosition(); View first = linearLayoutManager.findViewByPosition(firstVisibleIndex); int firstVisibleOffset = -first.getTop(); if (firstVisibleOffset == 0) { return; } if (firstVisibleOffset < itemSmallHeight / 2) { list.scrollBy(0, -firstVisibleOffset); } else { list.scrollBy(0, itemSmallHeight - firstVisibleOffset); } changeItemState(); } } });
The above is the complete sliding monitoring code. In onScrollStateChanged, judge whether the state is SCROLL_STATE_IDLE. If the sliding ends, judge the offset of the item displayed on the top, and select the rebound direction according to the offset. If the offset is small (most of the contents of the first item are displayed), scroll down to the top of the first item; otherwise, scroll up to the top of the second item.
This ensures that there must be an item fully highlighted at the top in the static state.
Finally, the changeItemState function is called to correct some errors.
To sum up
In fact, there are not many difficulties in the whole effect, mainly the understanding of RecyclerView sliding. At present, this version has a small problem in fast sliding.
In addition to the RecyclerView version, there is actually a ScrollView version of this effect. In fact, there are some problems in implementing this effect on ListView and RecyclerView. Therefore, I implemented a ScrollView that can be reused and recycled, used this custom ScrollView to achieve this effect, and customized the scroller for it to make it rebound with animation effect. At present, no problems have been found in the ScrollView version, but because many functions need to be implemented by ourselves and the overall code is complex, the RecyclerView version is selected to explain to you. If you are interested, you can go to the project on github and switch to tag v1 0, you can see the code of the ScrollView version.