现在更好的方式是使用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);