最初看到網易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;
}
複製程式碼