Android RecyclerView滑動居中banner實現

預覽

不少app的首頁都有一個能夠滑動的banner。大概長這樣:java

實現方式有多種,介紹一種用RecyclerView的實現方式git

實現思路

第一種平鋪的banner 其實很好實現,就是一個RecycerView + PagerSnapHelper。可是爲了兼容多種顯示效果,例如第二種的顯示效果,咱們須要去自定義LayoutMmanager和SnapHelper。github

LayoutManager部分

自定義LayoutManager通常是分兩步,佈局滑動,再想一想LayoutManager須要些什麼屬性。bash

屬性

須要一個boolean值標識是否循環佈局。 須要兩個float值標識滑動時的寬高縮放。app

public class BannerLayoutManager{
    private float heightScale = 0.9f;
    private float widthScale = 0.9f;
    private boolean infinite = true;  //默認無限循環
    
    ...
    
}
複製代碼

佈局

一、計算第一個View的開始位置 : int offsetX = (父佈局寬度 - 子View寬度) / 2ide

int offsetX = (mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(scrap)) / 2;
複製代碼

二、計算是否要添加一個view爲第1個子view,以顯示出循環佈局的效果。佈局

View lastChild = getChildAt(getChildCount() - 1);
   // 若是是循環佈局,而且最後一個view已超出父佈局,則添加最左邊的view
  if ( infinite && lastChild != null && getDecoratedRight(lastChild) > mOrientationHelper.getTotalSpace()) {
    layoutLeftItem(recycler);
  }
複製代碼

三、縮放全部的view
縮放規則:以父佈局的中線(中心線)爲基準,若是子view的中線與中心線重合,則縮放比爲1.0f;若是不重合,則計算出子view的中線與中心線的距離,距離越大,縮放比越小。動畫

private void scaleItem() {
        if (heightScale >= 1 || widthScale >= 1) {
            return;
        }

        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            float itemMiddle = (getDecoratedRight(child) + getDecoratedLeft(child)) / 2.0f;
            float screenMiddle = mOrientationHelper.getTotalSpace() / 2.0f;
            float interval = Math.abs(screenMiddle - itemMiddle) * 1.0f;

            float ratio = 1 - (1 - heightScale) * (interval / itemWidth);
            float ratioWidth = 1 - (1 - widthScale) * (interval / itemWidth);
            child.setScaleX(ratioWidth);
            child.setScaleY(ratio);
        }
    }
複製代碼

四、整體的佈局方法ui

private void layoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() == 0 || state.isPreLayout()) {
            removeAndRecycleAllViews(recycler);
            return;
        }
        detachAndScrapAttachedViews(recycler);

        View scrap = recycler.getViewForPosition(0);
        measureChildWithMargins(scrap, 0, 0);
        itemWidth = getDecoratedMeasuredWidth(scrap);
        int offsetX = (mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(scrap)) / 2;
        for (int i = 0; i < getItemCount(); i++) {
            if (offsetX > mOrientationHelper.getTotalSpace()) {
                break;
            }
            View viewForPosition = recycler.getViewForPosition(i);
            addView(viewForPosition);
            measureChildWithMargins(viewForPosition, 0, 0);
            offsetX += layoutItem(viewForPosition, offsetX);
        }

        View lastChild = getChildAt(getChildCount() - 1);
        // 若是是循環佈局,而且最後一個view已超出父佈局,則添加最左邊的view
        if ( infinite && lastChild != null && getDecoratedRight(lastChild) > mOrientationHelper.getTotalSpace()) {
            layoutLeftItem(recycler);
        }
        scaleItem();
    }
複製代碼

滑動

對滑動的處理就是爲了對view的回收,以減小消耗,提升效率。 處理方式就是根據滑動距離去添加和刪除view。this

我也是第一次自定義LayoutManager,感受寫得有點繁瑣了。分了左滑右滑兩種狀況去寫。

@Override
    public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
        return offsetDx(dx, recycler);
    }

    private int offsetDx(int dx, RecyclerView.Recycler recycler) {
        int realScroll = dx;
        // 向左
        if (dx > 0) {
            realScroll = scrollToLeft(dx, recycler, realScroll);
        }
        // 向右
        if (dx < 0) {
            realScroll = scrollToRight(dx, recycler, realScroll);
        }
        scaleItem();

        return realScroll;
    }
複製代碼

scrollToLeft或者scrollToRight都是隻作了三件事,添加view,計算實際滑動距離並滑動,回收view

private int scrollToLeft(int dx, RecyclerView.Recycler recycler, int realScroll) {
        while (true) {
            // 將須要添加的view添加到RecyclerView中
            View rightView = getChildAt(getChildCount() - 1);
            int decoratedRight = getDecoratedRight(rightView);
            if (decoratedRight - dx < mOrientationHelper.getTotalSpace()) {
                int position = getPosition(rightView);
                if (!infinite && position == getItemCount() - 1) {
                    break;
                }

                int addPosition = infinite ? (position + 1) % getItemCount() : position + 1;
                View lastViewAdd = recycler.getViewForPosition(addPosition);
                addView(lastViewAdd);
                measureChildWithMargins(lastViewAdd, 0, 0);
                int left = decoratedRight;
                layoutDecoratedWithMargins(lastViewAdd, left, getItemTop(lastViewAdd), left + getDecoratedMeasuredWidth(lastViewAdd), getItemTop(lastViewAdd) + getDecoratedMeasuredHeight(lastViewAdd));
            } else {
                break;
            }
        }

        // 處理滑動
        View lastChild = getChildAt(getChildCount() - 1);
        int left = getDecoratedLeft(lastChild);
        if (getPosition(lastChild) == getItemCount() - 1) {
            // 最後一個view已經到底了,計算實際能夠滑動的距離
            if (left - dx < 0) {
                realScroll = left;
            }
        }
        offsetChildrenHorizontal(-realScroll);

        // 回收滑出父佈局的view
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            int decoratedRight = getDecoratedRight(child);
            if (decoratedRight < 0) {
                removeAndRecycleView(child, recycler);
            }
        }
        return realScroll;
    }
複製代碼

這樣,自定義的LayoutManager基本就完成了。

SnapHelper部分

自定義完成了LayoutManager的確能夠高效的實現gif中的效果,可是滑動的時候就有問題了,RecyclerView默認是支持fling操做的,就是慣性滑動。而沒法作到一次只滑動一頁,而且居中顯示的效果(相似ViewPager的滑動效果)。
爲了實現這種效果,google提供了一個SnapHelper抽象類,咱們能夠繼承這個去實現本身的滑動邏輯。SDK提供了PagerSnapHelper和LinearSnapHelper兩種實現。
PagerSnapHelper能夠作到ViewPager那種一次滑動一頁的效果,可是當滑動到最後一個view的時候會明顯的出現卡頓。由於PagerSnapHelper默認不支持循環佈局這種狀況的。因此我繼承PagerSnaperHelper,修改了一點點邏輯,實現了循環滑動的效果。

public class BannerPageSnapHelper extends PagerSnapHelper {

    private boolean infinite = false;
    private OrientationHelper horizontalHelper;

    @Override
    public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
                                      int velocityY) {
        final int itemCount = layoutManager.getItemCount();
        if (itemCount == 0) {
            return RecyclerView.NO_POSITION;
        }

        View mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));

        if (mStartMostChildView == null) {
            return RecyclerView.NO_POSITION;
        }
        final int centerPosition = layoutManager.getPosition(mStartMostChildView);
        if (centerPosition == RecyclerView.NO_POSITION) {
            return RecyclerView.NO_POSITION;
        }

        final boolean forwardDirection;
        if (layoutManager.canScrollHorizontally()) {
            forwardDirection = velocityX > 0;
        } else {
            forwardDirection = velocityY > 0;
        }

        if (forwardDirection) {
            if (centerPosition == layoutManager.getItemCount() - 1) {
                return infinite ? 0 : layoutManager.getItemCount() - 1;
            } else {
                return centerPosition + 1;
            }
        } else {
            return centerPosition;
        }
    }

    private View findStartView(RecyclerView.LayoutManager layoutManager,
                               OrientationHelper helper) {
        int childCount = layoutManager.getChildCount();
        if (childCount == 0) {
            return null;
        }

        View closestChild = null;
        int start = Integer.MAX_VALUE;

        for (int i = 0; i < childCount; i++) {
            final View child = layoutManager.getChildAt(i);
            int childStart = helper.getDecoratedStart(child);

            /** if child is more to start than previous closest, set it as closest  **/
            if (childStart < start) {
                start = childStart;
                closestChild = child;
            }
        }
        return closestChild;
    }

    @NonNull
    private OrientationHelper getHorizontalHelper(
            @NonNull RecyclerView.LayoutManager layoutManager) {
        if (horizontalHelper == null) {
            horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
        }
        return horizontalHelper;
    }

    public boolean isInfinite() {
        return infinite;
    }

    public void setInfinite(boolean infinite) {
        this.infinite = infinite;
    }
}
複製代碼

擴展

關於banner部分,通常項目會有如下幾個參數。

一、style 展現樣式:例如圓角 或是平鋪。 能夠在每一個子view外面套一個CardView 去設置圓角,而後根據需求在adapter中設置view的寬高。

二、是否循環顯示:BannerLayoutManager和PagerHelper都有一個屬性,infinite,爲true時,循環顯示。

三、自動播放:這個在Activity或者Fragment中用Rxjava或者Handler加一個定時器,調用 recyclerView.smoothScrollToPosition(position)就好了 。

四、滑動動畫的顯示時間:BannerLayoutManager中有個smoothScrollTime屬性,調用set方法設置一下就好了。

應該能知足大多數需求吧。。

源碼

想了解詳情去看代碼吧 github.com/ZhangHao555…

相關文章
相關標籤/搜索