仿网易LOFTER视差滚动列表
原博客链接
最初看到网易LOFTER的首页的视差滚动效果, 觉得很漂亮, 想要模仿一下
在写代码之前我先百度了一下, 看有没有人已经完成了类似的这种效果, 一看果然有.然后我就把他们的代码clone了下来, 看了一下, 理解之后自己去实现了一番.所以本篇不是原创, 只记录原理和实现.以下是参考资料:
Android视图滚动差—ParallaxScrollImageView
高仿寺库View滑动页面
ParallaxRecyclerView
实现原理
首先需要写一个图片列表, 用listView或者recyclerView都可以.然后监听列表的滚动, 计算出图片的中心线和recyclerView的中心线之间的距离, 用这个距离乘以一个比例(这个比例自己定义, 效果合适即可)得到一个偏移量, 然后使用matrix给图片内容加上偏移量.
设置滚动监听
首先以recyclerView举例来说, 给它设置滚动监听
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(); // 获取第一个可见条目的position int firstVisibleItem = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition(); // 获取所有可见条目的数量 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 instanceof ParallaxViewHolder) { ParallaxViewHolder parallaxViewHolder = (ParallaxViewHolder) viewHolder; parallaxViewHolder.animateImage(); } } } });
上面代码中的ParallaxViewHolder是一个继承了RecyclerView.ViewHolder的自定义ViewHolder
public abstract class ParallaxViewHolder extends RecyclerView.ViewHolder implements ParallaxImageView.ParallaxImageListener { private ParallaxImageView mParallaxImageView; public abstract int getParallaxImageId(); public ParallaxViewHolder(View itemView) { super(itemView); mParallaxImageView = itemView.findViewById(getParallaxImageId()); mParallaxImageView.setListener(this); } public void animateImage() { mParallaxImageView.doTranslate(); } @Override public int[] requireValuesForTranslate() { if (itemView.getParent() == null) { return null; } else { int[] itemPosition = new int[2]; // 获取itemView左上角在屏幕上的坐标 itemView.getLocationOnScreen(itemPosition); int[] recyclerViewPosition = new int[2]; // 获取recyclerView在屏幕上的坐标 ((RecyclerView) itemView.getParent()).getLocationOnScreen(recyclerViewPosition); // 将参数传递过去 // itemView的高度, itemView在屏幕上的y坐标, recyclerView的高度, recyclerView在屏幕上的y坐标 return new int[]{itemView.getMeasuredHeight(), itemPosition[1], ((RecyclerView) itemView.getParent()).getHeight(), recyclerViewPosition[1]}; } } }
这里首先ParallaxViewHolder会获取ParallaxImageView(下面会说明)的id, 然后根据id获取parallaxImageView.然后给parallaxImageView设置回调方法, ParallaxViewHolder实现requireValuesForTranslate()方法, 在滚动的时候parallaxImageView会调用这个方法, 获取条目的高度
, 条目的在屏幕上的y坐标
, recyclerView的高度
, recyclerView在屏幕上的高度
这四个参数
ParallaxImageView
这个自定义控件是实现效果的重点.首先继承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() { setScaleType(ScaleType.MATRIX); } ......
可以看到初始化的时候, 给它的scaleType设置了matrix, 这是为什么呢?因为我们可以看到, 想要lofter那种效果, 是需要图片只露出一部分, 如下图中所示, 红框中代表ParallaxImageView, 蒙层部分表示不可见
系统提供的几种scaleType中, 没有一个能实现这种效果, 那就只能设置scaleType为ScaleType.MATRIX
, 然后自己使用maxtrix做变换了.关于scaleType, 如果还不熟悉, 可以看这篇文章Android ImageView的scaleType属性与adjustViewBounds属性ImageView的默认scaleType是fitcenter.设置scaleType为matrix之后, 会从ImageView的左上角开始绘制原图, 大概是像这个样子, 红色区域代表ParallaxImageView, 黑色区域代表图像.
设置缩放
首先要做的是计算一个缩放比例, 使缩放之后的drawable的宽度等于ParallaxImageView的宽度
/** * 重新计算ImageView的变换矩阵 * @return */ private float recomputeImageMatrix() { float scale; // 获取imageView的宽度减去padding之后的部分 final int viewWidth = getWidth() - getPaddingLeft() - getPaddingRight(); // 获取imageView的高度减去padding之后的部分 final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom(); // 获取drawable的宽度 final int drawableWidth = getDrawable().getIntrinsicWidth(); // 获取drawable的高度 final int drawableHeight = getDrawable().getIntrinsicHeight(); // 如果drawable的宽高比大于view的宽高比 // drawableWidth / drawableHeight > viewWidth / viewHeight if (drawableWidth * viewHeight > drawableHeight * viewWidth) { // 如果drawable的宽高比大于view的宽高比 // 那么就让drawable乘以一个scale, 使得drawable的高度能够等于view的高度, 使得drawable能够填充整个view // drawableHeight * (scale = viewHeight/ drawableHeight) = viewHeight scale = (float) viewHeight / (float) drawableHeight; } else { // 如果drawable的宽高比小于view的宽高比 <------ 代码会走这里 // 为了使drawable能够填充整个view, 需要使drawable的宽度能够等于view的宽度 // drawableWidth * (scale = viewWidth / drawableWidth) = viewWidth scale = (float) viewWidth / (float) drawableWidth; } return scale; }
然后按照这个比例进行变换
Matrix imageMatrix = getImageMatrix(); if (scale != 1) { imageMatrix.setScale(scale, scale); } setImageMatrix(imageMatrix); invalidate();
现在的效果是这样
使图片居中
接下来要做的是这种变换, 是视图内容居于ImageView的中间
首先计算出视图内容的中心线和ImageView中心线之间的距离
private float computeDistance(float scale) { // 获取imageView的高度减去padding之后的部分 final int viewHeight = getHeight() - getPaddingTop() - getPaddingBottom(); // 获取drawable的高度 int drawableHeight = getDrawable().getIntrinsicHeight(); // 按照比例变换后的drawableHeight drawableHeight *= scale; return viewHeight * 0.5f - drawableHeight * 0.5f; }
然后使用matrix的postTranslate()
方法进行y方向上的偏移
Matrix imageMatrix = getImageMatrix(); if (scale != 1) { imageMatrix.setScale(scale, scale); } float[] matrixValues = new float[9]; imageMatrix.getValues(matrixValues); // 获取当前的y值, 比如一开始y值是0, 目标是让当前的y值变为distance // 那么就在y方向上偏移 distance - currentY float currentY = matrixValues[Matrix.MTRANS_Y]; float dy = distance - currentY; imageMatrix.postTranslate(0, dy); setImageMatrix(imageMatrix);
变换后的效果, 可以看到已经居中了
加上偏移量
然后我们就可以计算每张图片的中线与列表的中线之间的距离, 然后乘以一个适当的比例设置给matrix
// translate是recyclerView中心线和itemView中心线之间的距离 float translate = (rvYPos + rvHeight * 0.5f) - (itemYPos + itemHeight * 0.5f); translate *= 0.2f; transform(scale, distance, translate); private void transform(float scale, float distance, float translate) { Matrix imageMatrix = getImageMatrix(); if (scale != 1) { imageMatrix.setScale(scale, scale); } float[] matrixValues = new float[9]; imageMatrix.getValues(matrixValues); // 获取当前的y值, 比如一开始y值是0, 目标是让当前的y值变为distance // 那么就在y方向上偏移 distance - currentY 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); }
现在的效果
边界修正
但是看上图的第二个条目, 把ImageView的红色背景露出来了(我给ImageView设置的红色的background).
如上图所示, 视图内容不断往下偏移(红色框框看成不动), 当在这种边界条件下视图内容继续往下偏移时, 就会把ImageView的背景露出来.所以计算然后限制边界条件
float maxTranslate = drawableHeight * 0.5f - viewHeight * 0.5f; float minTranslate = -maxTranslate; // translate是recyclerView中心线和itemView中心线之间的距离 float translate = (rvYPos + rvHeight * 0.5f) - (itemYPos + itemHeight * 0.5f); if (translate >= maxTranslate) { translate = maxTranslate; } else if (translate <= minTranslate) { translate = minTranslate; }
最终效果
github地址
github.com/mundane7996…