A list,

RecyclerView is now more and more powerful, not to mention has been used by everyone to replace the basic function of ListView, now RecyclerView can also replace ViewPager to achieve Banner effect, of course, The following small and fresh Gallery effect is similar to some of the effect of the rotation map, as shown in the figure below, which uses the auxiliary class SnapHelper added by RecyclerView after 24.2.0 version, in order to achieve the following effect is also very simple. So this is also why RecyclerView is powerful, because Google has been constantly updating and supplementing RecyclerView, so that its internal API is increasingly rich.

So let’s take horizontal sliding as an example and divide it into the following small problems:

  1. Each swipe keeps the image right in the middle.
  2. The left margin of the first image and the right margin of the last image should be the same as the left margin of the other photos.
  3. When you slide, the middle image goes from large to small when you slide to the left, and the right image goes from small to large when you slide to the middle.
  4. The background is gaussian blur.
  5. The slide ends with a gradient effect on the background, fading in and out of the previous image to the current image.

Second, implementation ideas

Of course, it is not difficult to solve the above problems. Let’s explain the realization idea step by step:

(1) Keep the image in the center with each slide

Keep the image centered. As mentioned in the introduction, after ToolsVersion24.2.0, Google gave us a helper class called SnapHelper, which takes just a few lines of code to help us keep the slide centered at the end:

LinearSnapHelper mLinearySnapHelper = new LinearSnapHelper();
mLinearySnapHelper.attachToRecyclerView(mGalleryRecyclerView);
Copy the code

The LinearSnapHelper class inherits from SnapHelper, and of course SnapHelper has a subclass called PagerSnapHelper. The difference between them is that the LinearSnapHelper allows RecyclerView to slide across multiple items at once, whereas the PagerSnapHelper, like ViewPager, limits you to sliding one Item at a time.

(2) The left margin of the first picture and the right margin of the last picture should be the same as the left margin of other photos

Because the image at position 0 and last position is special, all other images default to their margins and the visual distance between the left and right images. Since there is no image on the left of page 0, the left side is only 1 margin, which will look strange when you swipe to the far left, as shown below.

To keep the left side of the image at position 0 the same distance as the other images, you need to dynamically set the left side of the image at position 0 to 2 times the page margin + visual distance. Similarly, I’m going to do the same thing for the last slide.

Because of the reuse mechanism of RecyclerView to Holder, we’d better not dynamically modify the LayoutParams in Adapter. This is not elegant enough. Thank @w_binaryTree for its suggestion. Adding a custom Decoration to RecyclerView will make our code more elegant. Only need to rewrite RecyclerView. ItemDecoration getItemOffsets inside (the Rect outRect, final View View, final RecyclerView parent, Recyclerview. State State), and set the parameters of each page inside, modify as follows:

public class GalleryItemDecoration extends RecyclerView.ItemDecoration { int mPageMargin = 0; Int mLeftPageVisibleWidth = 50; Public static int mItemComusemX = 0; public static int mItemComusemX = 0; @override public void getItemOffsets(Rect outRect, final View View, final RecyclerView parent, RecyclerView.State state) { super.getItemOffsets(outRect, view, parent, state); / /... Int itemNewWidth = parent.getwidth () -dptopx (4 * mPageMargin + 2 * mLeftPageVisibleWidth); MItemComusemX = itemNewWidth + osutil. dpToPx(2 * mPageMargin); Int leftMargin = position == 0? Int leftMargin == 0? dpToPx(mLeftPageVisibleWidth + 2 * mPageMargin) : dpToPx(mPageMargin); int rightMargin = position == itemCount - 1 ? dpToPx(mLeftPageVisibleWidth + 2 * mPageMargin) : dpToPx(mPageMargin); / / set parameters RecyclerView. LayoutParams lp = (RecyclerView. LayoutParams) itemView. GetLayoutParams (); lp.setMargins(leftMargin, 0, rightMargin, 0); lp.width = itemWidth; itemView.setLayoutParams(lp); / /... } public int dpToPx(int dp) {return (int) (dp * resources.getSystem ().getDisplayMetrics().density + 0.5f); }}Copy the code

Then pass in GalleryItemDecoration:

mGalleryRecyclerView.addItemDecoration(new GalleryItemDecoration());
Copy the code

(3) When sliding, the middle picture changes from large to small when sliding to the left, and the right picture changes from small to large when sliding to the middle

This problem involves a lot of problems.

(a) Obtain the current position during sliding.

First of all, RecyclerView’s current API, it doesn’t allow us to simply swipe around and get the location of the middle image in our image, and you might say, Can pass mGalleryRecyclerView. GetLinearLayoutManager (.) findFirstVisibleItemPosition () can get the position of the first visible in the RecyclerView, but through effect can know, Each of our photos (except the first and last one) has parts of the previous photo and the last photo on the left and right sides, so we need to distinguish between the middle photo, the first photo or the last photo, And then return mGalleryRecyclerView. GetLinearLayoutManager (.) findFirstVisibleItemPosition () + 1 or other. So this leads to another question, when we put pictures show the width of the set can be configured, namely before and after photos showing part width is configurable, so when we put the screen does not display the pictures before and after the left part of the screen, then we can’t compatible with this method again, so a method to get through this, maybe less.

So we can figure out exactly where we are. In RecyclerView, we can listen for its sliding events:

/ / slide monitoring mGalleryRecyclerView. AddOnScrollListener (new RecyclerView. OnScrollListener () {@ Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); } @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); // Compute position by dx or dy. }});Copy the code

So there’s a method onScrolled(int dx, int dy), and the dx and dy in here are very useful. First of all, it can be judged that it slides up, down, left and right by judging whether dx and dy are greater than 0. Dx > 0 slides right, otherwise, dy > 0 slides left, and otherwise slides up (of course, my sliding here is relative to RecyclerView, that is, the sliding direction of the list, finger sliding direction is opposite to here). Second, dx and DY also listen for the distance consumed by each slide along the x and y axes.

For example, when we quickly get to the right side of the list, onScrolled(int dx, int dy) will be called repeatedly. By logging the output in the method, you will see that the dx values are constantly output, and their sizes are all irregular. Here, dx is called once every time the onScroll method is called. The consumption distance of RecyclerView on x axis.

So we can use a global variable mConsumeX to add all dx, and then we can know the total distance of the current RecyclerView slide. In our Demo, the distance to the next photo (i.e. the theoretical distance to move a page as shown in the figure below) is constant, so the position at the end of the slide can be obtained by the current position = mConsumeX/the distance needed to move a photo.

@param shouldConsumeX = @param shouldConsumeX = @param shouldConsumeX = @return private int getPosition(int mConsumeX, int shouldConsumeX) { float offset = (float) mConsumeX / (float) shouldConsumeX; int position = Math.round(offset); Return position; }Copy the code

(b) Get the sliding offset of the current page by position

When we can get an accurate picture of the current position, we need to clarify a few concepts.

Total offset: Means the total offset from the first position to the current position, which is the sum of dx (mConsumX above).

Current page offset: this means the offset from the previous position to the current position.

Total offset: means the total offset distance/the theoretical cost of moving a page.

Current page offset: this means the current page offset/the theoretical cost of moving a page.

As we all know, there’s one in the method to get the current location

float offset = (float) mConsumeX / (float) shouldConsumeX;
Copy the code

For example, if we move from 3 to 4, the onScroll method will be called constantly, and the offset will change from 3.0 to 4.0, and the offset will be about 3.2. What do we know about this offset? If offset is a floating point number and rounded down to 3, then 3.2-3 = 0.2 is the offset of our current page. And we can dynamically set the size of the picture through the offset rate, thus forming the image size change effect mentioned in this question. So the key here is to get the offset of the current page.

@Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { super.onScrolled(recyclerView, dx, dy); / /... / / move one page theory consumption distance int shouldConsumeX = GalleryItemDecoration. MItemComusemX; Int position = getPosition(mConsumeX, shouldConsumeX); // float offset = (float) mConsumeX/(float) shouldConsumeX; // Avoid integer integer of offset Which affect the percent value if (offset. > = mGalleryRecyclerView getLinearLayoutManager () findFirstVisibleItemPosition && () + 1 slideDirct == SLIDE_RIGHT) { return; } // Float percent = offset - ((int) offset; SetAnimation (recyclerView, Position, percent); / /... }Copy the code

(c) Animation based on offset rate

Now that we have the offset, we can change their size dynamically. First, we need to take the current View, the previous View, and the next View, and Scale them all at the same time. That is, the above setAnimation(recyclerView, Position, Percent) method for animation operations.

View mCurView = recyclerView.getLayoutManager().findViewByPosition(position); . / / intermediate page View mRightView = recyclerView getLayoutManager () findViewByPosition (position + 1); . / / the left page View mLeftView = recyclerView getLayoutManager () findViewByPosition (position 1); / / on the right side of the pageCopy the code

Observe the changes in the diagram carefully. There are two kinds of changes:

  1. Position changes: The first image is slowly changed from mCurView to mLeftView, and the second image is slowly changed from mRightView to mCurView.
  2. Size changes: The first picture is from large to small, and the second picture is from small to large.

Now that we understand the changes above, we can animate.

First of all, if you look at my getPosition(mConsumeX, shouldConsumeX) method, it will automatically switch to the next page when the offset of a slide exceeds 0.5. Of course, your implementation logic is different, so later you set the animation method is different. Why do we need to be clear about this? Since the mCurView above will switch to the next image when I swipe past half the width of the image, I set the animation method with 0.5 as a critical point, because the mCurView, mRightView, and mLeftView point on both sides are different.

If we define float mAnimFactor = 0.2f, it means scaling our image from 1.0 to 0.8. For example, when percent <= 0.5, the ScaleX and ScaleY of mCurView decrease from large to small. As for the range of change, we can modify according to the change factor and percent defined by us. When percent > 0.5, the View becomes mLeftView, and we continue with the operation. The Scale of the first image has changed from 1.0 to 0.8. The other two images are the same, and the code logic is as follows:

private void setBottomToTopAnim(RecyclerView recyclerView, int position, float percent) { View mCurView = recyclerView.getLayoutManager().findViewByPosition(position); . / / intermediate page View mRightView = recyclerView getLayoutManager () findViewByPosition (position + 1); . / / the left page View mLeftView = recyclerView getLayoutManager () findViewByPosition (position 1); If (percent <= 0.5) {if (mLeftView! Mleftview. setScaleX((1 - mAnimFactor) + percent * mAnimFactor); mLeftView.setScaleY((1 - mAnimFactor) + percent * mAnimFactor); } if (mCurView ! = null) {// Decrement McUrview. setScaleX(1 - percent * mAnimFactor); mCurView.setScaleY(1 - percent * mAnimFactor); } if (mRightView ! SetScaleX ((1 - mAnimFactor) + percent * mAnimFactor); mRightView.setScaleY((1 - mAnimFactor) + percent * mAnimFactor); } } else { if (mLeftView ! = null) { mLeftView.setScaleX(1 - percent * mAnimFactor); mLeftView.setScaleY(1 - percent * mAnimFactor); } if (mCurView ! = null) { mCurView.setScaleX((1 - mAnimFactor) + percent * mAnimFactor); mCurView.setScaleY((1 - mAnimFactor) + percent * mAnimFactor); } if (mRightView ! = null) { mRightView.setScaleX(1 - percent * mAnimFactor); mRightView.setScaleY(1 - percent * mAnimFactor); }}}Copy the code

(4) Background gaussian blur

There are many ways to implement Gaussian blur, Google came out. However, I still recommend the implementation algorithm of Native layer, because the implementation of Java layer has too great impact on performance. In the example, RenderScript is used, of course, it is a reference to the blogger qiushui’s teaching about dynamic blur effect in one minute. You can go to see if you are interested, and the usage is also very simple. Just call the blurBitmap(Context Context, Bitmap Image, float blurRadius) method.

Public class BlurBitmapUtil {private static final Float BITMAP_SCALE = 0.4f; Public static Bitmap ** @param context * @param image * @param image */ public static Bitmap blurBitmap(Context context, Bitmap image, Int width = math.round (image.getwidth () * BITMAP_SCALE); int height = Math.round(image.getHeight() * BITMAP_SCALE); / / will shrink after images as pre-rendered Bitmap inputBitmap = Bitmap. CreateScaledBitmap (image, width, height, false); Bitmap outputBitmap = bitmap.createBitMap (inputBitmap); RenderScript rs = renderscript.create (context); / / create a blur RenderScript tool object ScriptIntrinsicBlur blurScript = ScriptIntrinsicBlur. Create (rs, Element U8_4 (rs)); // Since RenderScript does not use VMS to allocate memory, we need to use Allocation class to create and allocate memory space. // When creating Allocation object, memory is empty, so we need to use copyTo() to fill in the Allocation tmpIn = Allocation.createFromBitmap(rs, inputBitmap); Allocation tmpOut = Allocation.createFromBitmap(rs, outputBitmap); Blurscript. setRadius(blurRadius); // Set the render blur to 25F. // Set the blurScript object's input memory blurScript.setinput (tmpIn); // Save the output data to blurScript.foreach (tmpOut); // Fill the Allocation with data tmpout.copyto (outputBitmap); return outputBitmap; }}Copy the code

This method simply passes in the Context, Bitmap, and a degree of blur, and then returns a Gaussian blurred Bitmap to us. All we need to do is set the parent of RecyclerView to this Bitmap.

(5) At the end of the slide, the background has a gradient effect, fading in and out of the previous image to the current image

It’s best not to use a Tween animation for this effect, because it’s a bit rough, and using a TransitionDrawable would be better to approximate the fade-in effect. So how do we record before and after photos? There are many ways to do this. Here, a Map

is used to record each image displayed, and as it switches to the next image, it fades in and out of the last recorded image to the current image.
,>

Int resourceId = ((RecyclerAdapter) int resourceId = (RecyclerAdapter) mRecyclerView.getAdapter()).getResId(mRecyclerView.getScrolledPosition()); / / the resource image into a Bitmap Bitmap resBmp = BitmapFactory. DecodeResource (getResources (), resourceId); / / will return after the Bitmap gaussian blur to resBlurBmp Bitmap resBlurBmp = BlurBitmapUtil. BlurBitmap (mRecyclerView. GetContext (), resBmp, 15 f); // resBlurBmp to Drawable Drawable resBlurDrawable = new BitmapDrawable(resBlurBmp); Drawable Drawable preBlurDrawable = mtsdracemap. get(KEY_PRE_DRAW) == null? resBlurDrawable : mTSDraCacheMap.get(KEY_PRE_DRAW); Drawable[] drawableArr = {preBlurDrawable, resBlurDrawable}; TransitionDrawable transitionDrawable = new TransitionDrawable(drawableArr); mContainer.setBackgroundDrawable(transitionDrawable); transitionDrawable.startTransition(500); Mtsdracachemap. put(KEY_PRE_DRAW, resBlurDrawable);Copy the code

More and more

What say above is a train of thought that realizes, although the effect and small pure and fresh take do not go up relation, but matched a few small pure and fresh picture or let our programmer life add a wonderful. In fact, after we achieve the basic effect, you can also dig more auxiliary functions, such as different switching effects, support landscape, dynamic change of sliding speed, I believe that this process can let you gain a lot.

Making: Recyclerview – Gallery