簡單實現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.重寫LayoutManager
的smoothScrollToPosition
方法使用自定義的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);