前段時間須要一個旋轉木馬效果用於展現圖片,因而第一時間在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的橫豎滾動事件還能夠作出更多有趣的效果。
最後,奉上源碼。