Original blog link

When I first saw the parallax scrolling effect on the homepage of netease LOFTER, I thought it was very beautiful and wanted to imitate it

Before writing the code, I did a Google search to see if anyone had done something similar, and sure enough, it did. Then I clone their code, look at it, understand it and implement it myself. Therefore, this article is not original, only documenting the principle and implementation. Here are some references:

Android view – ParallaxScrollImageView

High copy temple View slide page

ParallaxRecyclerView

Realize the principle of

First, you need to create a list of images, use listView or recyclerView. Then monitor the scrolling of the list, calculate the distance between the center line of the picture and recyclerView center line, multiply this distance by a proportion (this proportion is defined by themselves, the effect can be appropriate) to get an offset, and then use matrix to add the offset to the picture content.

Setting up scroll Listening

First to recyclerView, for example, to set up a scroll listening

mRv.addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {}@Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
                // Get position for the first visible entry
                int firstVisibleItem = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
                // Get the number of all visible entries
                int visibleItemCount = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition() - firstVisibleItem + 1;
                for (int i = 0; i < visibleItemCount; i++) {
                    View childView = recyclerView.getChildAt(i);
                    RecyclerView.ViewHolder viewHolder = recyclerView.getChildViewHolder(childView);
                    if (viewHolder instanceofParallaxViewHolder) { ParallaxViewHolder parallaxViewHolder = (ParallaxViewHolder) viewHolder; parallaxViewHolder.animateImage(); }}}});Copy the code

The ParallaxViewHolder in the code above is a custom ViewHolder that inherits RecyclerView.viewholder

public abstract class ParallaxViewHolder extends RecyclerView.ViewHolder implements ParallaxImageView.ParallaxImageListener {
    private ParallaxImageView mParallaxImageView;

    public abstract int getParallaxImageId(a);
    public ParallaxViewHolder(View itemView) {
        super(itemView);
        mParallaxImageView = itemView.findViewById(getParallaxImageId());
        mParallaxImageView.setListener(this);

    }

    public void animateImage(a) {
        mParallaxImageView.doTranslate();
    }

    @Override
    public int[] requireValuesForTranslate() {
        if (itemView.getParent() == null) {
            return null;
        } else {
            int[] itemPosition = new int[2];
            // Get the onscreen coordinates of the upper-left corner of itemView
            itemView.getLocationOnScreen(itemPosition);
            int[] recyclerViewPosition = new int[2];
            // Get the recyclerView coordinates on the screen
            ((RecyclerView) itemView.getParent()).getLocationOnScreen(recyclerViewPosition);
            // Pass the parameter
            // The height of itemView, the y of itemView on the screen, the height of recyclerView, the Y of recyclerView on the screen
            return new int[]{itemView.getMeasuredHeight(), itemPosition[1], ((RecyclerView) itemView.getParent()).getHeight(), recyclerViewPosition[1]}; }}}Copy the code

So first ParallaxViewHolder will get the ID ParallaxImageView, and then ParallaxImageView will get the ID ParallaxImageView. Then give parallaxImageView set the callback method, implemented ParallaxViewHolder requireValuesForTranslate () method, the rolling parallaxImageView will invoke this method, obtaining the height of the entry, The y coordinate of the entry on the screen, the height of recyclerView, the height of recyclerView on the screen these four parameters

ParallaxImageView

This custom control is the focus of the effect. First inherit ImageView

public class ParallaxImageView extends AppCompatImageView {
   private static final String TAG = "ParallaxImageView";
   private int itemHeight;
   private int itemYPos;
   private int rvHeight;
   private int rvYPos;

   public ParallaxImageView(Context context) {
       super(context);
       init();
   }

   public ParallaxImageView(Context context, AttributeSet attrs) {
       super(context, attrs);
       init();
   }

   public ParallaxImageView(Context context, AttributeSet attrs, int defStyle) {
       super(context, attrs, defStyle);
       init();
   }

   private void init(a) { setScaleType(ScaleType.MATRIX); }...Copy the code

You can see that when it initializes, it sets the matrix to its scaleType. Why is that? Because as we can see, the lofter effect requires that only part of the image be exposed, as shown below, ParallaxImageView in red, and the masked part invisible



If none of the scaleTypes provided by the system can achieve this effect, you must set scaleType toScaleType.MATRIX, and then use Maxtrix to do the transformation yourself. If you’re not familiar with scaleType, check out this articleAdjustViewBounds property and scaleType property for Android ImageViewThe default scaleType for ImageView is FitCenter. When you set scaleType to matrix, it will draw the original image from the upper left corner of the ImageView, something like this, the red area represents ParallaxImageView, and the black area represents the image.

Set the zoom

The first thing to do is calculate a scale so that the width of the scaled Drawable is equal to the width of the ParallaxImageView

    /** * Recalculate the ImageView transformation matrix *@return* /
    private float recomputeImageMatrix(a) {
        float scale;
        // Get the imageView width minus the padding
        final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight();
        // Get the imageView height minus the padding
        final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
        // Get the width of drawable
        final int drawableWidth = getDrawable().getIntrinsicWidth();
        // Get the height of the drawable
        final int drawableHeight = getDrawable().getIntrinsicHeight();

        // If drawable is larger than view
        // drawableWidth / drawableHeight > viewWidth / viewHeight
        if (drawableWidth * viewHeight > drawableHeight * viewWidth) {
            // If drawable is larger than view
            // Then multiply the drawable by a scale, so that the height of the drawable equals the height of the view, so that the drawable fills the view
            // drawableHeight * (scale = viewHeight/ drawableHeight) = viewHeight
            scale = (float) viewHeight / (float) drawableHeight;
        } else { // If the drawable is less than the view <------ the code will go here

            // In order for the drawable to fill the entire view, the width of the drawable needs to be equal to the width of the view
            // drawableWidth * (scale = viewWidth / drawableWidth) = viewWidth
            scale = (float) viewWidth / (float) drawableWidth;
        }

        return scale;
    }
Copy the code

And then I’m going to do it in proportion

        Matrix imageMatrix = getImageMatrix();
        if(scale ! =1) {
            imageMatrix.setScale(scale, scale);
        }
        setImageMatrix(imageMatrix);
        invalidate();

Copy the code

Here’s what it looks like

Center the picture

So what we’re going to do is we’re going to do this transformation, and we’re going to put the view content in the middle of the ImageView



Start by calculating the distance between the center line of the view content and the center line of the ImageView

    private float computeDistance(float scale) {
        // Get the imageView height minus the padding
        final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom();
        // Get the height of the drawable
        int drawableHeight = getDrawable().getIntrinsicHeight();

        // Scale the drawableHeight
        drawableHeight *= scale;
        return viewHeight * 0.5 f - drawableHeight * 0.5 f;

    }
Copy the code

The y direction is then offset using the postTranslate() method of matrix

        Matrix imageMatrix = getImageMatrix();
        if(scale ! =1) {
            imageMatrix.setScale(scale, scale);
        }
        float[] matrixValues = new float[9];
        imageMatrix.getValues(matrixValues);
        // Get the current y value, such as 0 at the beginning, and the goal is to change the current y value to distance
        // Offset distance-currenty in the y direction
        float currentY = matrixValues[Matrix.MTRANS_Y];
        float dy = distance - currentY;
        imageMatrix.postTranslate(0, dy);
        setImageMatrix(imageMatrix);
Copy the code

And when you transform it, you can see it’s already centered

Plus the offset

Then we can calculate the distance between the center line of each image and the center line of the list and multiply it by an appropriate scale set to the matrix

    Translate is the distance between recyclerView centerline and itemView centerline
    float translate = (rvYPos + rvHeight * 0.5 f) - (itemYPos + itemHeight * 0.5 f);
    translate *= 0.2 f;
    transform(scale, distance, translate);
        
    private void transform(float scale, floatdistancefloat translate) {
        Matrix imageMatrix = getImageMatrix();
        if(scale ! =1) {
            imageMatrix.setScale(scale, scale);
        }
        float[] matrixValues = new float[9];
        imageMatrix.getValues(matrixValues);
        // Get the current y value, such as 0 at the beginning, and the goal is to change the current y value to distance
        // Offset distance-currenty in the y direction
        float currentY = matrixValues[Matrix.MTRANS_Y];
        float dy = translate + distance - currentY;
        int position = (int) getTag(R.id.tag_position);
        if (position == 1) {
            Log.d(TAG, "translate = " + translate);
        }
        imageMatrix.postTranslate(0, dy);
        setImageMatrix(imageMatrix);
    }
Copy the code

Current effect

Boundary correction

But look at the second entry in the image above, which shows the red background of the ImageView (the red background I set for the ImageView).



As shown in the figure above, the view content is continuously offset down (the red box is still), and when the view content is continuously offset down under these boundary conditions, the ImageView background is exposed. So compute and then restrict the boundary conditions

        float maxTranslate = drawableHeight * 0.5 f - viewHeight * 0.5 f;
        float minTranslate = -maxTranslate;
        Translate is the distance between recyclerView centerline and itemView centerline
        float translate = (rvYPos + rvHeight * 0.5 f) - (itemYPos + itemHeight * 0.5 f);
        if (translate >= maxTranslate) {
            translate = maxTranslate;
        } else if (translate <= minTranslate) {
            translate = minTranslate;
        }
Copy the code

The final result

Making the address

Github.com/mundane7996…