Horsepower in Android or once again on RecyclerView.LayoutManager

3r3133. Horsepower in Android or once again on RecyclerView.LayoutManager 3r33737.
 3r33724. According to the author, the article can be useful in the same way as it is for novice Android developers making their first steps in such an exciting area. The history of the subject of this note began with the idea to equip the educational project with the so-called “wow effect”. How much it was possible to judge you. I ask all curious under kat.
 3r33724. 3r311.
 3r33724. A demo project with all this ugliness can be found on GitHub at 3r3-3695. link
.
 3r33724.
 3r33724. At the core of the screen that interests us is the well-loved RecyclerView. And the highlight is the fact that when scrolling through the list, one fully visible top element scaled in a special way. This feature is characterized by the fact that the scaling occurs in different ways for the components that make up the list item.
 3r33724.
 3r33724. However, it is better to see once.
 3r33724.
 3r33724. 3r? 3551. 3r33552. Fig. 1. General view of 3r35353. 3r35454. 3r3133. 3r3334. 3r33737.
 3r33724. 3r33737. 3r33737.
 3r33724. Consider the list item in detail. In the project, it is implemented as a LaunchItemView class inherited from CardView. Its markup contains the following components:
 3r33724.
 3r33724. 3r33535.  3r33724. 3r337. Image - the ScaledImageView class, designed to render an image with a given height (scaling). 3r33737.  3r33724. 3r337. Headline 3r33737.  3r33724. 3r337. Explanatory text. 3r33737.  3r33724. 3r33540.
 3r33724. 3r3133. 3r361 3r33737.
 3r33724. [i] Fig. 2. The structure of the list item (LaunchItemView). 3r33140.
 3r33724.
 3r33724. In the process of scrolling the list, the following happens: 3r3703.  3r33724.
 3r33724.
 3r33724. 3r337. The element height changes from the minimum value (equal to the height of the heading with decoration) to the maximum value (equal to the height that allows displaying the title and explanatory text, with decoration). 3r33737.  3r33724. 3r337. The height of the image is equal to the height of the element minus decoration, the width varies proportionally. 3r33737.  3r33724. 3r337. The relative position inside the element and the size of the explanatory text remains unchanged. 3r33737.  3r33724. 3r337. The magnitude of the scaling is limited to the top with a minimum size sufficient to display all the content, taking into account the decoration and below with a minimum size sufficient to display the title, taking into account the decoration. 3r33737.  3r33724. 3r337. Scaling, other than boundary values, is applied to the top fully visible list item. The elements above it have a maximum scale, below - the minimum. 3r33737.  3r33724. 3r33737.
 3r33724. Thus, when scrolling up, the effect of gradual “extrusion” of the contents of the element with proportional scaling of the image is created. When scrolling down, the opposite effect is observed.
 3r33724.
 3r33724. I solved the problem posed in this way by creating a LayoutManager for RecyclerView and two additional components. But first things first.
 3r33724.
 3r33724.

LaunchLayoutManager


 3r33724. My training project is devoted to space topics, so the components have received the appropriate names.
 3r33724.
 3r33724. Studying the topic of creating an arbitrary LayoutManager, I found two good articles on this topic,[1, 2]. I will not repeat their contents. Instead, I’ll focus on the most interesting points of my decision.
 3r33724.
 3r33724. Performing the decomposition of the task, I singled out two main stages: 3r3703.  3r33724.
 3r33724.
 3r33724. 3r337. The definition of the index of the first and last element, which are fully or partially visible on the screen. 3r33737.  3r33724. 3r337. Rendering of visible elements with the necessary scaling. 3r33737.  3r33724. 3r33737.
 3r33724. In general, the location and size of the elements of the list looks like this:
 3r33724.
 3r33724. 3r3133. 3r33737.
 3r33724. [i] Figure 3. RecyclerView and its elements. 3r33140.
 3r33724.
 3r33724.

Definition of a closed interval of indices of visible elements


 3r33724. In fig. 3 visible elements include indexes from 3 to 11 inclusive. Moreover, according to our algorithm, the elements with indices 0-3 have a maximum size, the elements 5-12 have a minimum size, and an element with an index of 4 is intermediate between the minimum and maximum.
 3r33724.
 3r33724. As you might guess, one of the key points in determining the minimum and maximum index of visible elements is the offset by which the list is swept relative to the upper boundary of the visible area.
 3r33724.
 3r33724. Consider the method calculateVisiblePositions, designed to determine these values.
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private void calculateVisiblePositions () {
2 if (mBigViewHeight! = 0) {
3 mMaximumOffset = (getItemCount () - 1) * mBigViewHeight; 3r33724. 4 mFirstVisibleViewPosition = mOffset /mBigViewHeight; 3r33724. 5 if (mFirstVisibleViewPosition> getItemCount () - 1) {
6 mFirstVisibleViewPosition = 0; 3r33724. 7 mOffset = 0; 3r33724. 8} 3r32424. 9
10 mLastVisibleViewPosition = mFirstVisibleViewPosition; 3r33724. 11 int emptyHeight = getHeight (); 3r33724. 12 mFirstVisibleViewTopValue =
mBigViewHeight * mFirstVisibleViewPosition - mOffset; 3r33724. 13 int firstVisibleViewBottomValue =
mFirstVisibleViewTopValue + mBigViewHeight; 3r33724. 14 emptyHeight - = firstVisibleViewBottomValue; 3r33724. 15 int secondVisibleViewHeight =
getViewHeightByTopValue (firstVisibleViewBottomValue); 3r33724. 16 if (emptyHeight - secondVisibleViewHeight> = 0) {
17 emptyHeight - = secondVisibleViewHeight; 3r33724. 18 mLastVisibleViewPosition ++; 3r33724. 19 int smallViewPosCount = emptyHeight /mSmallViewHeight; 3r33724. 20 mLastVisibleViewPosition + = smallViewPosCount; 3r33724. 21 emptyHeight - = smallViewPosCount * mSmallViewHeight; 3r33724. 22 if (emptyHeight> 0) {3r33724. 23 mLastVisibleViewPosition ++; 3r33724. 24} 3r3244. 25} 3r3724. 26 if (mLastVisibleViewPosition> getItemCount () - 1) {
27 mLastVisibleViewPosition = getItemCount () - 1; 3r33724. 28} 3r3244. 29 Timber.d ("calculateVisiblePositions
MFirstVisibleViewPosition:% d,
MLastVisibleViewPosition:% d",
MFirstVisibleViewPosition,
MLastVisibleViewPosition); 3r33724. 30} 3r32424. 31} 3r3244. 3r3675.
 3r33724. Line 2 - check if the height of the element of maximum size is determined, which displays all the content (title, explanatory text and image). If not, then it makes no sense to continue.
 3r33724.
 3r33724. Line 3 - we calculate how many places, except for one item scrolled up, will take up the maximum allowable offset. This value will limit the offset value in the scrollVerticallyBy method.
 3r33724.
 3r33724. Line 4 - calculate the index of the first visible element. Since the mFirstVisibleViewPosition variable is of integer type, due to the drop of the fractional part, we automatically take into account the case of the partially visible first element.
 3r33724.
 3r33724. Lines 5-8 - checks if the index of the first visible item exceeds the index of the last item in the list. This can happen, for example, when the list was first scrolled up, and then the number of elements decreased, for example, by applying a filter. In this case, simply “rewind” the list to the beginning.
 3r33724.
 3r33724. Line 10 - use the index of the first visible element as a starting point to search for the index of the last.
 3r33724.
 3r33724. Line 11 - set the height of the visible area. This value will decrease during the search for the maximum index of visible elements.
 3r33724.
 3r33724. Lines 1? 13 - we define the coordinates top and bottom of the rectangle of drawing the first element.
 3r33724.
 3r33724. Line 14 - we reduce the free visible area by the size of the visible part of the first element. Those. as if we are virtually placing the first element on the screen.
 3r33724.
 3r33724. Line 15 - calculate the height of the second visible element. This element is potentially subject to scaling (see paragraph 5 of the algorithm). The getViewHeightByTopValue method is described in detail below.
 3r33724.
 3r33724. Line 16 - check if there is still free space after the “virtual placement” of the second element on the screen.
 3r33724.
 3r33724. Line 17 - we fix how much free space is left.
 3r33724.
 3r33724. Line 18 - increment the index of the last visible item.
 3r33724.
 3r33724. Line 19 - we calculate the largest number of elements of the minimum size that can fit in the remaining free space and at the same time will be fully visible.
 3r33724.
 3r33724. Line 20 - increase the index of the last visible element by the calculated value.
 3r33724.
 3r33724. Lines 21-24 - check if there is room for partial placement of another element. If so, increase the index by one more.
 3r33724.
 3r33724. Now about the method that calculates the height of the second visible element depending on the position on the screen - the top coordinates of the display rectangle of this element.
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private int getViewHeightByTopValue (int topValue) {
2 topValue - = mTopAndBottomMargins; 3r33724. 3 if (topValue> mBigViewHeight) {
4 topValue = mBigViewHeight; 3r33724. 5} else if (topValue < 0) {
6 topValue = 0;
7}
8 float scale = 1 - (float) topValue /mBigViewHeight;
9 int height =
(Int) (mSmallViewHeight +). - mSmallViewHeight));
10 Timber.d ("getViewHeightByTopValue
topValue:% d, scale:% f, height:% d",
topValue, scale, height);
11 height;
737 d.}
3r336755.
 3r33724. Line 2 - drop the lower and upper margin.
 3r33724.
 3r33724. Lines 3-7 - for correct calculation of the scale, we limit the top from the top to the maximum height of the element, and from below to zero.
 3r33724.
 3r33724. Line 8 - we calculate the scaling factor, which takes the value 1 for the maximally expanded element and 0 for the minimum. For the correctness of this particular result, we need restrictions in lines 3-7.
 3r33724.
 3r33724. Line 9 - we calculate the height of the element as an increase to the minimum height and the difference between the maximum and minimum height, taking into account the scaling factor. Those. when the coefficient is ? the height is minimal, and when 1 - minimum + (maximum - minimum) = maximum.
 3r33724.
 3r33724. So now we know the first and last indexes of the elements to be drawn. It's time to do it!
 3r33724.
 3r33724.

Drawing elements with the necessary scaling 3r3702.
 3r33724. Since the process of drawing is cyclical, then immediately before drawing we will warm up the cache with already existing RecyclerView elements (if there are any, of course). This technique is covered in[1, 2]and here I will not dwell on it.
 3r33724.
 3r33724. Consider the fillDown method, designed to draw elements moving from top to bottom along the available visible area.
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private void fillDown (RecyclerView.Recycler recycler) {
2 boolean isViewFromCache; 3r33724. 3 int topValue = mFirstVisibleViewTopValue; 3r33724. 4 int bottomValue; 3r33724. 5 int viewHeight; 3r33724. 6 try {
7 for (int curPos = mFirstVisibleViewPosition;
CurPos <= mLastVisibleViewPosition; curPos++) {
8 isViewFromCache = true;
9 View view = mViewCache.get (curPos); 3rrr2424. 10 if (view == null) {3r3373724. .12 view = recycler.getViewForPosition (curPos);
13} else {
14 mViewCache.remove (curPos);
15}
16 viewHeight = getViewHeightByTopValue (topValue); 3rrrrrrrrrrrrrrrrrr = 3rrrr = 3rrr =
r.
18 if (view instanceof LaunchItemView) {
19 ((LaunchItemView) view) .updateContentSize (viewHeight);
20}
21 if (isViewFromCache) {
22 if (view.rtT)) ) {
23 view.setTop (topValue);
24}
25 if (view.getBottom ()! = BottomValue - mTopAndBottomMargins) {
26 view.setBottom (bottomValue - mTopAndBottomMargins); 3r33724. 27} 3r3244. 28 attachView (view); 3r33724. 29} else {
30 layoutView (view, topValue, bottomValue); 3r33724. 31} 3r3244. 32 topValue = bottomValue; 3r33724. 33} 3r32424. 34} catch (Throwable throwable) {
35 Timber.d (throwable); 3r33724. 36} 3r32424. 37} 3r3244. 3r3675.
 3r33724. Line 3 - we initiate the variable topValue by the coordinate of the top of the first visible element. From the stove of this value and will continue to dance.
 3r33724.
 3r33724. Line 7 - we initiate a loop on the indices of the elements to be drawn.
 3r33724.
 3r33724. Line 8 - optimistically we assume that we will find the item we need in the cache.
 3r33724.
 3r33724. Line 9 - look at the cache (with hope).
 3r33724.
 3r33724. Line 10-12 - if the item we need is not in the cache, we request it from an instance of the RecyclerView.Recycler class, which returns a view initialized with data from the adapter for a specific position.
 3r33724.
 3r33724. Line 14 - if the item was still in the cache, delete it from there.
 3r33724.
 3r33724. Line 16 - we calculate the height of the element depending on its position on the screen.
 3r33724.
 3r33724. Line 17 - calculate the lower boundary of the element.
 3r33724.
 3r33724. Lines 18-20 - we scale the content of the element, if it knows how to do it.
 3r33724.
 3r33724. Line 21 - it is important for us to understand whether the current view was previously drawn (taken from the cache) or whether we received a new instance. These two options require different approaches.
 3r33724.
 3r33724. Lines 22-28 - if the view is obtained from the cache, then, if necessary, we change the values ​​of the coordinates top and bottom, and append the view.
 3r33724.
 3r33724. Line 30 - if the view is not from the cache, then to display the element we use the layoutView method, which is described below.
 3r33724.
 3r33724. Line 32 - we shift the topValue to the lower border of the view just drawn so that this value becomes the starting point for the next iteration of the loop.
 3r33724.
 3r33724. Now about the layoutView method for displaying a new list item obtained from an instance of the RecyclerView.Recycler class.
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private void layoutView (View view, int top, int bottom) {
2 addView (view); 3r33724. 3 measureChildWithMargins (view, ? 0); 3r33724. 4 int decoratedMeasuredWidth = getDecoratedMeasuredWidth (view); 3r33724. 5 RecyclerView.LayoutParams layoutParams =
(RecyclerView.LayoutParams) view.getLayoutParams (); 3r33724. 6
7 layoutDecorated (view,
8 layoutParams.leftMargin,
9 top + layoutParams.topMargin,
10 decoratedMeasuredWidth + layoutParams.rightMargin,
11 bottom + layoutParams.bottomMargin); 3r33724. 12} 3r32424. 3r3675.
 3r33724. Line 2 - add view to RecyclerView.
 3r33724.
 3r33724. Line 3 - measure the view.
 3r33724.
 3r33724. Line 4 - define the width of the view.
 3r33724.
 3r33724. Line 5 - we get the layout view parameters.
 3r33724.
 3r33724. Line 7 - we actually draw the view in the received coordinates.
 3r33724.
 3r33724.
Scaling content


 3r33724. Of the entire structure of the list item, only the image is subject to scaling. The logic of this scaling is encapsulated in the ScaledImageView class inherited from View.
 3r33724.
 3r33724. In our case, the execution of scaling is required at arbitrary points in time, and depends on external factors that we cannot control, for example, on how intensively the user scrolls the list. Since this very organically falls into the paradigm of reactive programming, I could not miss the chance to practice with RxJava and the hot data source.
 3r33724.
 3r33724. We will use PublishProcessor to create a stream of integer values ​​that determine the desired image height:
 3r33724.
 3r33724. 3r3661. 3r3662. private PublishProcessor
mScalePublishProcessor; 3r3675.
 3r33724. Accordingly, to perform scaling, we simply generate another stream element with the required value:
 3r33724.
 3r33724. 3r3661. 3r3662. public void setImageHeight (int height) {
mScalePublishProcessor.onNext (height); 3r33724.}
3r3675.
 3r33724. This is how asynchronous processing of this stream takes place:
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private void initObserver () {
2 mScalePublishProcessor
3 .filter (value -> value> 0
&& value! = MBitmapHeight
&& mOriginalBitmap! = Null)
4 .onBackpressureBuffer (? 3r32424. 5 () -> Timber.d ("initObserver: buffer overflow"),
BackpressureOverflowStrategy.DROP_OLDEST)
6 .observeOn (Schedulers.computation (), false, 1)
7 .map (this :: createScaledBitmap)
8 .map (this :: setScaledBitmap)
9 .subscribe (
10 (value) -> {
11 invalidate ();
12 Timber.d ("initObserver invalidate ThreadId:% d", 3rr3724. Thread.currentThread (). GetId ());
.13}, Timber :: d); 3r33724. 14} 3r3244. 3r3675.
 3r33724. Line 3 - perform the initial filtering:
 3r33724.
 3r33724. 3r33535.  3r33724. 3r337. Drop values ​​less than or equal to zero. 3r33737.  3r33724. 3r337. We do not consider values ​​whose value is equal to the height of the image scaled earlier. Those. the case when scaling is no longer required. 3r33737.  3r33724. 3r337. Do not perform scaling when the original image is not initialized. Initialization is discussed below in the setBitmap method. 3r33737.  3r33724. 3r33540.
 3r33724. Line 4 - use back pressure with a buffer size of 1 element and a strategy of displacing the older element from the buffer. Due to this, we will always receive the most current value of the height for scaling. In our case, this is very important, because we have a hot source that, in response to user actions (for example, intensive scrolling of the list), will generate elements faster than we can process them (perform scaling). In such conditions, it makes no sense to accumulate values ​​in the buffer and process these elements sequentially, since they are already outdated, the user has already “squandered” this state.
 3r33724.
 3r33724. To illustrate and enhance the effect, I added a delay of 25 ms to the image scaling method (createScaledBitmap) and below resulted in two visualizations: without using back pressure (left) and with back pressure (right). The interface on the left is clearly lagging behind the actions of the user, living some kind of life. Right - lost in smoothness due to the additional delay in the scaling method, but not in responsiveness.
 3r33724.
 3r33724. 3r? 3551. 3r33552. Comparison 3r35353. 3r35454. 3r33555.  3r33724. 3r33557.  3r33724. 3r33535. Without back pressure 3r33565.  3r33724. 3r33535. With back pressure 3r33535. 3r33565.  3r33724. 3r3-3567.  3r33724. 3r? 3569.
 3r33724. 3r33737. 3r33737.
 3r33724. Line 6 - we transfer work to the Schedulers.computation () stream with indication of the buffer size.
 3r33724.
 3r33724. Line 7 - we perform scaling (see the description of the method below).
 3r33724.
 3r33724. Line 8 - set the scaled image to display.
 3r33724.
 3r33724. Line 9 - subscribe to the stream.
 3r33724.
 3r33724. Line 11 - at the end of scaling we redraw the element.
 3r33724.
 3r33724. The createScaledBitmap method directly involved in obtaining an image of the desired size:
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private Bitmap createScaledBitmap (Integer value) {
2 Timber.d ("createScaledBitmap value:% d", value); 3r33724. 3 if (value> mHeightSpecSize) {
4 value = mHeightSpecSize; 3r33724. 5} 3r32424. 6 return Bitmap.createScaledBitmap (mOriginalBitmap, value, value, false); 3r33724. 7} 3r32424. 3r3675.
 3r33724. Lines 3-5 - we will limit the maximum height to the size of view, which is calculated in the onMeasure method.
 3r33724.
 3r33724. Line 6 - create an image of the desired size from the original.
 3r33724.
 3r33724. In the setScaledBitmap method, we save the scaled image for displaying in view:
 3r33724.
 3r33724. 3r3661. 3r3662. 1 private Boolean setScaledBitmap (Bitmap bitmap) {
2 try {3r33724. 3 mBitmapLock.lock (); 3r33724. 4 if (bitmap! = MDrawBitmap && mDrawBitmap! = Null) {
5 mDrawBitmap.recycle (); 3r33724. 6} 3r32424. 7 mDrawBitmap = bitmap; 3r33724. 8 mBitmapHeight = bitmap.getHeight (); 3r33724. 9} catch (Throwable throwable) {
10 Timber.d (throwable); 3r33724. 11} 3r3244. 12 mBitmapLock.unlock (); 3r33724. 13 return true; 3r33724. 14} 3r3244. 3r3675.
 3r33724. Lines ? 12 - we use a lock to synchronize the access to the variable containing the image that is to be drawn on the screen.
 3r33724.
 3r33724. Lines 4-6 - recycle the previously created image.
 3r33724.
 3r33724. Lines 7-8 - remember the new image and its size.
 3r33724.
 3r33724. The setBitmap method sets the original image:
 3r33724.
 3r33724. 3r3661. 3r3662. 1 public void setBitmap (Bitmap bitmap) {
2 if (bitmap! = Null) {
3 mOriginalBitmap = Bitmap.createScaledBitmap (bitmap,
MWidthSpecSize, mHeightSpecSize, false); 3r33724. 4 if (bitmap! = MOriginalBitmap) {
5 bitmap.recycle (); 3r33724. 6} 3r32424. 7 int height = mBitmapHeight; 3r33724. 8 mBitmapHeight = 0; 3r33724. 9 setImageHeight (height); 3r33724. 10} 3r32424. 11} 3r3244. 3r3675.
 3r33724. Line 3 - we scale the original image to the size of the view. This will allow us to save resources when scaling in the createScaledBitmap method if the original image exceeds the view in size.
 3r33724.
 3r33724. Line 4-6 - recycle the old original image.
 3r33724.
 3r33724. Lines 7-9 - reset the height for scaling to overcome the filter in the initObserver method, and produce a stream element to redraw the new image at the desired scale.
 3r33724.
 3r33724.

The result is


 3r33724. As part of the article, I tried to clearly explain some of the ideas that came to me during the work on the educational project. The demo project can be found on GitHub at 3r3-39595. link
. With comments, suggestions, suggestions and criticism I ask in the comments.
 3r33724.
 3r33724.

Related links


 3r33724.
 3r33724. 3r337. Recipes for Android: How to make a delicious LayoutManager 3r33737.  3r33724. 3r337. 3r33737. Building a RecyclerView LayoutManager - Part 1
3r33737.  3r33724. 3r33737. 3r33737. 3r33724. 3r33724. 3r33724.
3r33724. 3r33737.
+ 0 -

Comments 1

Offline
Neon
Neon 22 February 2019 14:19
处理您的编码因为这是我们需要关注的第一件事。https://essaypinglun.wordpress.com/easy-essay-org-评论/ 有很多你需要观看的课程。

Add comment