1. 程式人生 > >簡單實現Google play 橫向RecyclerListView效果

簡單實現Google play 橫向RecyclerListView效果

現在更好的方式是使用SnapHelper 在RecyclerView 24.2.0 支援庫之後新增使用方法

需要實現的功能

這裡只實現回彈的效果 和 在一個寬度內顯示2個半item的效果。

分析

下面是需要實現的效果:

這裡寫圖片描述

1.看起來就是一個橫向的ListView,現在有我們可以容易的使用RecyclerView並配合LinearLayoutManager 實現一個橫向的ListView

2.需要支援回彈效果,RecyclerView 本身擁有的scrollToPosition(int targetPosition)smoothScrollToPosition(int targetPosition)

,目前看來很簡單。

實現

好吧,看起來沒什麼可分析的。為了方便使用 自定義一個HorizontalRecyclerView繼承自 RecyclerView

HorizontalRecyclerView

public class HorizontalRecyclerView extends RecyclerView {
    private LinearLayoutManager mLayoutManager;

    public HorizontalRecyclerView(Context context) {
        super(context);
        init(context);
    }

    public HorizontalRecyclerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public HorizontalRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context context){
        mLayoutManager = new LinearLayoutManager(context);//自定義的LinearLayoutManager extends LinearLayoutManager
        mLayoutManager.setOrientation(android.support.v7.widget.LinearLayoutManager.HORIZONTAL);
        setLayoutManager(mLayoutManager);
        addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                switch (newState){
                    case SCROLL_STATE_IDLE://
                        int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition();
                        int firstCompletelyVisibleItem = mLayoutManager.findFirstCompletelyVisibleItemPosition();
                        int lastCompletelyVisibleItem = mLayoutManager.findLastCompletelyVisibleItemPosition();
                        if(lastCompletelyVisibleItem == getAdapter().getItemCount()-1) return;
                        if(firstCompletelyVisibleItem == firstVisibleItem) return;
                        View firstItem = mLayoutManager.findViewByPosition(firstVisibleItem);
                        if(Math.abs(firstItem.getLeft())*2>firstItem.getWidth()) {
                            smoothScrollToPosition(firstCompletelyVisibleItem);
                        }else {
                            smoothScrollToPosition(firstVisibleItem);
                        }
                        break;
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
            }
        });
    }
}

就是做一個初始化工作,設定一個橫向的LinearLayoutManager,並且新增滑動監聽。在監聽裡判斷需要滑到哪個位置,執行滑動。

執行之後發現,並沒有進行滑動。下面是我解決的方案:

1.重寫LayoutManagersmoothScrollToPosition方法使用自定義的MyLinearSmoothScroller 代替LinearLayoutManager預設的scroller。

 @Override
    public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
        MyLinearSmoothScroller linearSmoothScroller =
                new MyLinearSmoothScroller(recyclerView.getContext()) {
                    @Override
                    public PointF computeScrollVectorForPosition(int targetPosition) {
                        return LinearLayoutManager.this
                                .computeScrollVectorForPosition(targetPosition);
                    }
                };
        linearSmoothScroller.setTargetPosition(position);
        startSmoothScroll(linearSmoothScroller);
    }

2.MyLinearSmoothScroller 繼承自LinearSmoothScroller重寫下面兩個方法,第一個是為了使移動能夠發生,第二個是控制滑動速度。

 @Override
    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int snapPreference) {
        switch (snapPreference) {
            case SNAP_TO_START:
                return boxStart - viewStart;
            case SNAP_TO_END:
                return boxEnd - viewEnd;
            case SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
//                if (dtStart > 0) {
                    return dtStart;
//                }
//                final int dtEnd = boxEnd - viewEnd;
//                if (dtEnd < 0) {
//                    return dtEnd;
//                }
//                break;
            default:
                throw new IllegalArgumentException("snap preference should be one of the"
                        + " constants defined in SmoothScroller, starting with SNAP_");
        }
//        return 0;
    }
@Override
    protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
         return MILLISECONDS_PER_INCH / displayMetrics.densityDpi;//返回的是移動一個畫素 需要的毫秒數
    }

3.控制一次佈局展示可以展現 2.5個Item,重寫LinearLayoutManager的測量子view的方法

@Override
    public void measureChildWithMargins(View child, int widthUsed, int heightUsed) {
        final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
//
//        final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
//        widthUsed += insets.left + insets.right;
//        heightUsed += insets.top + insets.bottom;
//
//        final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
//                getPaddingLeft() + getPaddingRight() +
//                        lp.leftMargin + lp.rightMargin + widthUsed, lp.width,
//                canScrollHorizontally());
        final int widthSpec = getChildMeasureSpec((int) (0.4*getWidth()),getWidthMode(),
                0,lp.width,canScrollHorizontally());
        final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                getPaddingTop() + getPaddingBottom() +
                        lp.topMargin + lp.bottomMargin + heightUsed, lp.height,
                canScrollVertically());
//        if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
            child.measure(widthSpec, heightSpec);
//        }
    }

其他

那麼為什麼之前呼叫滑動,沒有進行滑動呢。還是看這個方法

 /**
     * Helper method for {@link #calculateDxToMakeVisible(android.view.View, int)} and
     * {@link #calculateDyToMakeVisible(android.view.View, int)}
     */
    public int calculateDtToFit(int viewStart, int viewEnd, int boxStart, int boxEnd, int
            snapPreference) {
        switch (snapPreference) {
            case SNAP_TO_START:
                return boxStart - viewStart;
            case SNAP_TO_END:
                return boxEnd - viewEnd;
            case SNAP_TO_ANY:
                final int dtStart = boxStart - viewStart;
                if (dtStart > 0) {
                    return dtStart;
                }
                final int dtEnd = boxEnd - viewEnd;
                if (dtEnd < 0) {
                    return dtEnd;
                }
                break;
            default:
                throw new IllegalArgumentException("snap preference should be one of the"
                        + " constants defined in SmoothScroller, starting with SNAP_");
        }
        return 0;
    }

我們觸發滑動時會穿過去的snapPreference == SNAP_TO_ANY 然後不滿足下面兩個if條件 最後返回 0。然後snapPreference 是個什麼?如果能保證snapPreference==SNAP_TO_START 就不用重寫這個方法了。看下面兩個方法註釋

 /**
     * When the target scroll position is not a child of the RecyclerView, this method calculates
     * a direction vector towards that child and triggers a smooth scroll.
     *
     * @see #computeScrollVectorForPosition(int)
     */
    protected void updateActionForInterimTarget(Action action) {
        // find an interim target position
        PointF scrollVector = computeScrollVectorForPosition(getTargetPosition());
        if (scrollVector == null || (scrollVector.x == 0 && scrollVector.y == 0)) {
            Log.e(TAG, "To support smooth scrolling, you should override \n"
                    + "LayoutManager#computeScrollVectorForPosition.\n"
                    + "Falling back to instant scroll");
            final int target = getTargetPosition();
            action.jumpTo(target);
            stop();
            return;
        }
        normalize(scrollVector);
        mTargetVector = scrollVector;

        mInterimTargetDx = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.x);
        mInterimTargetDy = (int) (TARGET_SEEK_SCROLL_DISTANCE_PX * scrollVector.y);
        final int time = calculateTimeForScrolling(TARGET_SEEK_SCROLL_DISTANCE_PX);
        // To avoid UI hiccups, trigger a smooth scroll to a distance little further than the
        // interim target. Since we track the distance travelled in onSeekTargetStep callback, it
        // won't actually scroll more than what we need.
        action.update((int) (mInterimTargetDx * TARGET_SEEK_EXTRA_SCROLL_RATIO)
                , (int) (mInterimTargetDy * TARGET_SEEK_EXTRA_SCROLL_RATIO)
                , (int) (time * TARGET_SEEK_EXTRA_SCROLL_RATIO), mLinearInterpolator);
    }
 /**
         * 

RecyclerView will call this method each time it scrolls until it can find the target * position in the layout.

*

SmoothScroller should check dx, dy and if scroll should be changed, update the * provided {@link Action} to define the next scroll.

* * @param dx Last scroll amount horizontally * @param dy Last scroll amount verticaully * @param state Transient state of RecyclerView * @param action If you want to trigger a new smooth scroll and cancel the previous one, * update this object. */ abstract protected void onSeekTargetStep(int dx, int dy, State state, Action action);