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…