仿網易LOFTER視差滾動列表

mundane發表於2019-03-04

原部落格連結

最初看到網易LOFTER的首頁的視差滾動效果, 覺得很漂亮, 想要模仿一下
仿網易LOFTER視差滾動列表
在寫程式碼之前我先百度了一下, 看有沒有人已經完成了類似的這種效果, 一看果然有.然後我就把他們的程式碼clone了下來, 看了一下, 理解之後自己去實現了一番.所以本篇不是原創, 只記錄原理和實現.以下是參考資料:
Android檢視滾動差—ParallaxScrollImageView
高仿寺庫View滑動頁面
ParallaxRecyclerView

實現原理

首先需要寫一個圖片列表, 用listView或者recyclerView都可以.然後監聽列表的滾動, 計算出圖片的中心線和recyclerView的中心線之間的距離, 用這個距離乘以一個比例(這個比例自己定義, 效果合適即可)得到一個偏移量, 然後使用matrix給圖片內容加上偏移量.
仿網易LOFTER視差滾動列表

設定滾動監聽

首先以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, 蒙層部分表示不可見
仿網易LOFTER視差滾動列表
系統提供的幾種scaleType中, 沒有一個能實現這種效果, 那就只能設定scaleType為ScaleType.MATRIX, 然後自己使用maxtrix做變換了.
關於scaleType, 如果還不熟悉, 可以看這篇文章
Android ImageView的scaleType屬性與adjustViewBounds屬性
ImageView的預設scaleType是fitcenter.
設定scaleType為matrix之後, 會從ImageView的左上角開始繪製原圖, 大概是像這個樣子, 紅色區域代表ParallaxImageView, 黑色區域代表影像.
仿網易LOFTER視差滾動列表

設定縮放

首先要做的是計算一個縮放比例, 使縮放之後的drawable的寬度等於ParallaxImageView的寬度
仿網易LOFTER視差滾動列表

    /**
     * 重新計算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();

複製程式碼

現在的效果是這樣
仿網易LOFTER視差滾動列表

使圖片居中

接下來要做的是這種變換, 是檢視內容居於ImageView的中間
仿網易LOFTER視差滾動列表
首先計算出檢視內容的中心線和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);
複製程式碼

變換後的效果, 可以看到已經居中了
仿網易LOFTER視差滾動列表

加上偏移量

然後我們就可以計算每張圖片的中線與列表的中線之間的距離, 然後乘以一個適當的比例設定給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);
    }
複製程式碼

現在的效果

仿網易LOFTER視差滾動列表

邊界修正

但是看上圖的第二個條目, 把ImageView的紅色背景露出來了(我給ImageView設定的紅色的background).
仿網易LOFTER視差滾動列表
如上圖所示, 檢視內容不斷往下偏移(紅色框框看成不動), 當在這種邊界條件下檢視內容繼續往下偏移時, 就會把ImageView的背景露出來.所以計算然後限制邊界條件
仿網易LOFTER視差滾動列表

        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;
        }
複製程式碼

最終效果

仿網易LOFTER視差滾動列表

github地址

github.com/mundane7996…

相關文章