In this article we want to share the experience of creating a custom ViewGroup in Android, which we developed in the framework of one of the projects of the program "Common frontal system". Our task was to create a beautiful gallery of bank cards. At the same time, the usual list provided by RecyclerView and LinearLayoutManager did not fit. There was an idea to show non-standard mechanics of card scrolling, so that when a card transitions, they would not go completely off the screen, but would be collected in a pile. Read about how we did it under the cut.
In the background we say that our first option was trivial - to use a ready-made solution. For example, in Android for a long time there is a similar StackView control. We will not cite the code, it is quite simple, we are looking for an Activity StackView, we’re sending an adapter to it, which gives the View to our maps. See what happens. Maps are located diagonally, plus some kind of strange animation. Not at all what we would like. In the customization of this class for a long time to understand, so try it yourself. ')
List mechanics
Through trial and error we arrived at mechanics, where maps are displayed in a form similar to the list. In this case, cards that are not visible in the usual list, when they go beyond its limits, we stack up in a pile. Here it is important to limit the use of memory, more precisely, to keep in memory not all child View, but the minimum, preferably a constant amount.
For simplicity, we describe the mechanics of the stack from the top when scrolling up. The bottom stack will work in much the same way - only the cards will slip under it, and not run over. The red line in the figure shows where the border of the beginning of the stack passes.
For further work, we introduce the notation:
foldHeight - height of the area for the stack;
maxCardCountInFold - the maximum possible number of cards in a stack, in our example, it is three;
cardFoldHeight = foldHeight / maxCardCountInFold - the height of the card in the stack.
List states
On the screen are whole cards. One by one. All as in the usual list.
We start scrolling up. The blue card starts to hit the green card. It stops at the position when the visible part of the green card becomes equal to cardFoldHeight. Now there is one card in the stack.
We continue to scroll up. The first two cards do not move. The pink card is coming in on the blue. It stops at the position when the visible part of the blue card under it becomes equal to cardFoldHeight. Now there are two cards in the pile.
Scroll on. Only now the visible part of the pink card becomes equal to cardFoldHeight. In this state, there are three cards in the stack - the maximum number of cards allowed. To add a new card to the stack, you need to remove the first added card from it. Strictly speaking, the stack works according to the FIFO principle: first entered, first went.
At what point do you start moving the entire stack to throw the first card overboard? Consider the possible options:
a) we begin to move the stack at the moment when the turquoise card came into contact with the yellow card;
b) we start moving the stack at the moment when the turquoise card has already hit the yellow one, and the yellow one has the size cardFoldHeight, point A.
In both cases, the stack moves when the yellow card is moved from point A to point B. When the turquoise card is in position B, the blue card becomes completely invisible. In this state, our StackView frees the memory occupied by the blue card.
In our implementation, we chose the second option, since visually moving maps in this case looks smoother.
Internal StackView
We briefly describe the main components of our custom ViewGroup, and how they interact.
Helper classes
// , class Range { private int mFrom; private int mTo; }
// // currentScroll. public class RangeCalculator { public Range getVisibleRange(int currentScroll); }
// // — currentScroll. class Fold { public int minTop(); public int maxTop(); public void update(int currentScroll, int fullCardHeight); }
Now StackView itself
StackView is the heir to the ViewGroup. In our StackView in the dispatchTouchEvent method (MotionEvent event) with the help of the heir GestureDetector.SimpleOnGestureListener we determine when the user scrolls the list and the offset is currentScroll. The currentScroll parameter will determine the position of the cards in the list.
The main methods of the StackView class, which determine the sizes and positions of the child View, are onMeasure () and onLayout (). Below is the pseudocode of these methods.
@OverrideprotectedvoidonMeasure(int widthMeasureSpec, int heightMeasureSpec){ super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); mFold.update(mCurrentScroll, mFullCardHeight); final Range newRange = mRangeCalculator.getVisibleRange(mCurrentScroll); if (getChildCount() == 0) { addCards(newRange); } else { removeCards(newRange); addNewCards(newRange); } mVisibleCardsRange.set(newRange); } }
@OverrideprotectedvoidonLayout(boolean changed, int left, int top, int right, int bottom){ finalint childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final View child = getChildAt(i); finalint childLeft = getPaddingLeft(); finalint childRight = childLeft + child.getMeasuredWidth(); finalint childHeight = child.getMeasuredHeight(); finalint childTop = getChildTop(childCount, i, childHeight); finalint childBottom = childTop + childHeight; child.layout(childLeft, childTop, childRight, childBottom); } }
// childTop private int getChildTop(final int childCount, final int childIndex, int childHeight) { int childTop = -mCurrentScroll + (childIndex + mVisibleCardsRange.from()) * childHeight + getPaddingTop(); int minTopForCurrentChild = (int) (childIndex * mFold.getCardSizeInFold()) - mFold.minTop(); minTopForCurrentChild = Math.max(0, minTopForCurrentChild); int maxTopForCurrentChild = (int) (getMeasuredHeight() - (childCount - childIndex) * mFold.getCardSizeInFold()) + mFold.maxTop(); maxTopForCurrentChild = Math.min(maxTopForCurrentChild, getMeasuredHeight()); if (childTop < minTopForCurrentChild) childTop = minTopForCurrentChild; if (childTop > maxTopForCurrentChild) childTop = maxTopForCurrentChild; return childTop; }
What happened and what conclusions we made
We started creating this custom ViewGroup component from scratch, based on the implementation of a similar list. But I had to spend time studying someone else's code and finishing it to the state we needed. In the process, we made several options for implementing the mechanics of the list and eventually chose one that looks beautiful and at the expense of some simplifications consumes a limited amount of memory - in this case, we simply limit the number of cards in the stack.
In practice, it turned out that there is no point in showing all the cards in a pile. If there are too many of them, the size of the visible part of the card in the stack tends to zero, and this does not add any beauty. We have a constant number of child View, and we are not afraid of a gray wolf OutOfMemoryException.
We can assume that we have coped with the task of building a prototype. We identified a variant of the mechanics of the list, which looks good, and most importantly - technically implementable. And now we know how to make it better.
We will be glad to talk with you and exchange ideas on the subject. We decided not to post all the code in the post, but to stay on the basic principles. If you have any questions, please write in the comments.