前段时间须要一个旋转木马效果用于展现图片,因而第一时间在github上找了一圈,找了一个还不错的控件,可是使用起来有点麻烦,始终以为很不爽,因此寻思着本身作一个轮子。想起旋转画廊的效果不是和横向滚动列表很是类似吗?那么是否能够利用RecycleView实现呢?git
RecyclerView是google官方在support.v7中提供的一个控件,是ListView和GridView的升级版。该控件具备高度灵活、高度解耦的特性,而且还提供了添加、删除、移动的动画支持,分分钟让你做出漂亮的列表、九宫格、瀑布流。相信使用过该控件的人一定爱不释手。github
先来看下如何简单的使用RecyclerViewbash
RecyclerView listView = (RecyclerView)findViewById(R.id.lsit);
listView.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false));
listView.setAdapter(new Adapter());复制代码
就是这么简单:ide
其中,LayoutManager用于指定布局管理器,官方已经提供了几个布局管理器,能够知足大部分需求:布局
Adapter的定义与ListView的Adapter用法相似。动画
重点来看LayoutManage。this
LinearLayoutManager与其余几个布局管理器都是继承了该类,从而实现了对每一个Item的布局。那么咱们也能够经过自定义LayoutManager来实现旋转画廊的效果。google
看下要实现的效果: spa
首先,咱们来看看,自定义LayoutManager是什么样的流程:code
处理滑动事件(包括横向和竖向滚动、滑动结束、滑动到指定位置等)
i.横向滚动:重写scrollHorizontallyBy()方法
ii.竖向滚动:重写scrollVerticallyBy()方法
iii.滑动结束:重写onScrollStateChanged()方法
iiii.指定滚动位置:重写scrollToPosition()和smoothScrollToPosition()方法
接下来,就来实现这个流程
public class CoverFlowLayoutManger extends RecyclerView.LayoutManager {
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
}复制代码
继承LayoutManager后,会强制要求必须实现generateDefaultLayoutParams()方法,提供默认的Item布局参数,设置为Wrap_Content,由Item本身决定。
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//若是没有item,直接返回
//跳过preLayout,preLayout主要用于支持动画
if (getItemCount() <= 0 || state.isPreLayout()) {
mOffsetAll = 0;
return;
}
mAllItemFrames.clear(); //mAllItemFrame存储了全部Item的位置信息
mHasAttachedItems.clear(); //mHasAttachedItems存储了Item是否已经被添加到控件中
//获得子view的宽和高,这里的item的宽高都是同样的,因此只须要进行一次测量
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
//计算测量布局的宽高
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
//计算第一个Item X轴的起始位置坐标,这里第一个Item居中显示
mStartX = Math.round((getHorizontalSpace() - mDecoratedChildWidth) * 1.0f / 2);
//计算第一个Item Y轴的启始位置坐标,这里为控件竖直方向居中
mStartY = Math.round((getVerticalSpace() - mDecoratedChildHeight) *1.0f / 2);
float offset = mStartX; //item X轴方向的位置坐标
for (int i = 0; i < getItemCount(); i++) { //存储全部item具体位置
Rect frame = mAllItemFrames.get(i);
if (frame == null) {
frame = new Rect();
}
frame.set(Math.round(offset), mStartY, Math.round(offset + mDecoratedChildWidth), mStartY + mDecoratedChildHeight);
mAllItemFrames.put(i, frame); //保存位置信息
mHasAttachedItems.put(i, false);
//计算Item X方向的位置,即上一个Item的X位置+Item的间距
offset = offset + getIntervalDistance();
}
detachAndScrapAttachedViews(recycler);
layoutItems(recycler, state, SCROLL_RIGHT); //布局Item
mRecycle = recycler; //保存回收器
mState = state; //保存状态
}复制代码
以上,咱们为Item的布局作了准备,计算了Item的宽高,以及首个Item的起始位置,并根据设置的Item间,计算每一个Item的位置,并保存了下来。
接下来,来看看layoutItems()方法作了什么。
private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state, int scrollDirection) {
if (state.isPreLayout()) return;
Rect displayFrame = new Rect(mOffsetAll, 0, mOffsetAll + getHorizontalSpace(), getVerticalSpace()); //获取当前显示的区域
//回收或者更新已经显示的Item
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int position = getPosition(child);
if (!Rect.intersects(displayFrame, mAllItemFrames.get(position))) {
//Item没有在显示区域,就说明须要回收
removeAndRecycleView(child, recycler); //回收滑出屏幕的View
mHasAttachedItems.put(position, false);
} else { //Item还在显示区域内,更新滑动后Item的位置
layoutItem(child, mAllItemFrames.get(position)); //更新Item位置
mHasAttachedItems.put(position, true);
}
}
for (int i = 0; i < getItemCount(); i++) {
if (Rect.intersects(displayFrame, mAllItemFrames.get(i)) &&
!mHasAttachedItems.get(i)) { //加载可见范围内,而且尚未显示的Item
View scrap = recycler.getViewForPosition(i);
measureChildWithMargins(scrap, 0, 0);
if (scrollDirection == SCROLL_LEFT || mIsFlatFlow) {
//向左滚动,新增的Item须要添加在最前面
addView(scrap, 0);
} else { //向右滚动,新增的item要添加在最后面
addView(scrap);
}
layoutItem(scrap, mAllItemFrames.get(i)); //将这个Item布局出来
mHasAttachedItems.put(i, true);
}
}
}
private void layoutItem(View child, Rect frame) {
layoutDecorated(child,
frame.left - mOffsetAll,
frame.top,
frame.right - mOffsetAll,
frame.bottom);
child.setScaleX(computeScale(frame.left - mOffsetAll)); //缩放
child.setScaleY(computeScale(frame.left - mOffsetAll)); //缩放
}复制代码
第一个方法:在layoutItems()中
mOffsetAll记录了当前控件滑动的总偏移量,一开始mOffsetAll为0。
在第一个for循环中,先判断已经显示的Item是否已经超出了显示范围,若是是,则回收改Item,不然更新Item的位置。
在第二个for循环中,遍历了全部的Item,而后判断Item是否在当前显示的范围内,若是是,将Item添加到控件中,并根据Item的位置信息进行布局。
第二个方法:在layoutItem()中
调用了父类方法layoutDecorated对Item进行布局,其中mOffsetAll为整个旋转控件的滑动偏移量。
布局好后,对根据Item的位置对Item进行缩放,中间最大,距离中间越远,Item越小。
因为旋转画廊只需横向滚动,因此这里只处理横向滚动事件复制代码
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mAnimation != null && mAnimation.isRunning()) mAnimation.cancel();
int travel = dx;
if (dx + mOffsetAll < 0) {
travel = -mOffsetAll;
} else if (dx + mOffsetAll > getMaxOffset()){
travel = (int) (getMaxOffset() - mOffsetAll);
}
mOffsetAll += travel; //累计偏移量
layoutItems(recycler, state, dx > 0 ? SCROLL_RIGHT : SCROLL_LEFT);
return travel;
}复制代码
首先,须要告诉RecyclerView,咱们须要接收横向滚动事件。
当用户滑动控件时,会回调scrollHorizontallyBy()方法对Item进行从新布局。
咱们先忽略第一句代码,mAnimation用于处理滑动中止后Item的居中显示。
而后,咱们判断了滑动距离dx,加上以前已经滚动的总偏移量mOffsetAll,是否超出全部Item能够滑动的总距离(总距离= Item个数 * Item间隔),对滑动距离进行边界处理,并将实际滚动的距离累加到mOffsetAll中。
当dx>0时,控件向右滚动,即<--;当dx<0时,控件向左滚动,即-->复制代码
接着,调用先前已经写好的布局方法layoutItems(),对Item进行从新布局。
最后,返回实际滑动的距离。
@Override
public void onScrollStateChanged(int state) {
super.onScrollStateChanged(state);
switch (state){
case RecyclerView.SCROLL_STATE_IDLE:
//滚动中止时
fixOffsetWhenFinishScroll();
break;
case RecyclerView.SCROLL_STATE_DRAGGING:
//拖拽滚动时
break;
case RecyclerView.SCROLL_STATE_SETTLING:
//动画滚动时
break;
}
}
private void fixOffsetWhenFinishScroll() {
//计算滚动了多少个Item
int scrollN = (int) (mOffsetAll * 1.0f / getIntervalDistance());
//计算scrollN位置的Item超出控件中间位置的距离
float moreDx = (mOffsetAll % getIntervalDistance());
if (moreDx > (getIntervalDistance() * 0.5)) { //若是大于半个Item间距,则下一个Item居中
scrollN ++;
}
//计算最终的滚动距离
int finalOffset = (int) (scrollN * getIntervalDistance());
//启动居中显示动画
startScroll(mOffsetAll, finalOffset);
//计算当前居中的Item的位置
mSelectPosition = Math.round (finalOffset * 1.0f / getIntervalDistance());
}复制代码
经过onScrollStateChanged()方法,能够监听到控件的滚动状态,这里咱们只需处理滑动中止事件。
在fixOffsetWhenFinishScroll()中,getIntervalDistance()方法用于获取Item的间距。
根据滚动的总距离除以Item的间距计算出总共滚动了多少个Item,而后启动居中显示动画。
private void startScroll(int from, int to) {
if (mAnimation != null && mAnimation.isRunning()) {
mAnimation.cancel();
}
final int direction = from < to ? SCROLL_RIGHT : SCROLL_LEFT;
mAnimation = ValueAnimator.ofFloat(from, to);
mAnimation.setDuration(500);
mAnimation.setInterpolator(new DecelerateInterpolator());
mAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffsetAll = Math.round((float) animation.getAnimatedValue());
layoutItems(mRecycle, mState, direction);
}
});
}复制代码
动画很简单,从滑动中止的位置,不断刷新Item布局,直到滚动到最终位置。
@Override
public void scrollToPosition(int position) {
if (position < 0 || position > getItemCount() - 1) return;
mOffsetAll = calculateOffsetForPosition(position);
if (mRecycle == null || mState == null) {
//若是RecyclerView还没初始化完,先记录下要滚动的位置
mSelectPosition = position;
} else {
layoutItems(mRecycle, mState,
position > mSelectPosition ? SCROLL_RIGHT : SCROLL_LEFT);
}
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
if (position < 0 || position > getItemCount() - 1) return;
int finalOffset = calculateOffsetForPosition(position);
if (mRecycle == null || mState == null) {
//若是RecyclerView还没初始化完,先记录下要滚动的位置
mSelectPosition = position;
} else {
startScroll(mOffsetAll, finalOffset);
}
}复制代码
scrollToPosition()用于不带动画的Item直接跳转
smoothScrollToPosition()用于带动画Item滑动
也很简单,计算要跳转Item的所在位置须要滚动的距离,若是不须要动画,则直接对Item进行布局,不然启动滑动动画。
当从新调用RecyclerView的setAdapter时,须要对LayoutManager的全部状态进行重置
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
removeAllViews();
mRecycle = null;
mState = null;
mOffsetAll = 0;
mSelectPosition = 0;
mLastSelectPosition = 0;
mHasAttachedItems.clear();
mAllItemFrames.clear();
}复制代码
清空全部的Item,已经全部存放的位置信息和状态。
最后RecyclerView会从新调用onLayoutChildren()进行布局。
以上,就是自定义LayoutManager的流程,可是,为了实现旋转画廊的功能,只自定义了LayoutManager是不够的。旋转画廊中,每一个Item是有重叠部分的,所以会有Item绘制顺序的问题,若是不对Item的绘制顺序进行调整,将出现中间Item被旁边Item遮挡的问题。
为了解决这个问题,须要重写RecyclerView的getChildDrawingOrder()方法,对Item的绘制顺序进行调整。
这里简单看下如何如何改变Item的绘制顺序,具体能够查看源码复制代码
public class RecyclerCoverFlow extends RecyclerView {
public RecyclerCoverFlow(Context context) {
super(context);
init();
}
public RecyclerCoverFlow(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public RecyclerCoverFlow(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
......
setChildrenDrawingOrderEnabled(true); //开启从新排序
......
}
@Override
protected int getChildDrawingOrder(int childCount, int i) {
//计算正在显示的全部Item的中间位置
int center = getCoverFlowLayout().getCenterPosition()
- getCoverFlowLayout().getFirstVisiblePosition();
if (center < 0) center = 0;
else if (center > childCount) center = childCount;
int order;
if (i == center) {
order = childCount - 1;
} else if (i > center) {
order = center + childCount - 1 - i;
} else {
order = i;
}
return order;
}
}复制代码
首先,须要调用setChildrenDrawingOrderEnabled(true); 开启从新排序功能。
接着,在getChildDrawingOrder()中,childCount为当前已经显示的Item数量,i为item的位置。
旋转画廊中,中间位置的优先级是最高的,两边item随着递减。所以,在这里,咱们经过以上定义的LayoutManager计算了当前显示的Item的中间位置,而后对Item的绘制进行了从新排序。
最后将计算出来的顺序优先级返回给RecyclerView进行绘制。
以上,经过旋转画廊控件,咱们过了一遍自定义LayoutManager的流程。固然RecyclerView的强大远远不至于此,结合LayoutManager的横竖滚动事件还能够作出更多有趣的效果。
最后,奉上源码。