1.前言
????我們都知道,對于RecyclerView而言,android自帶的有三種類型的LayoutManager,分別是LinearLayoutManagr(線性布局器),GridLayoutManager(網(wǎng)格布局器)和StaggeredGridLayoutManager(瀑布流布局器)。然而在實際的開發(fā)過程中,這三種往往不能夠滿足實際效果的需要,那么就需要開發(fā)者自己去打造自己的LayoutManager。網(wǎng)上找了很多篇的博客和文章,自己研究并實現(xiàn)了下如下的效果,在這里分享給大家。
2.RecyclerView機制
????要想打造屬于自己的LayoutManager,首先得了解下RecyclerView的機制是什么樣的。關(guān)于RecyclerView的機制是什么樣的以及最基礎(chǔ)的LayoutManager的改造方法,大家可以看csdn地址http://blog.csdn.net/huachao1001/article/details/51594004 這篇文章。這篇是我看的文章里面描述最詳細的。我在這邊主要講述如何實現(xiàn)上述的效果
3.效果的分析
????從上面的列表效果可以看出,RecyclerView支持的是左右進行滑動,滑動方式是沿著一條曲線進行,并且在經(jīng)過中心位置的時候item有個放大縮小的效果。那么從列表的布局來看,我們可以將其排布在一個圓形弧線上,中心位置的角度為0度,左側(cè)遞減,右側(cè)遞增,那么每個item的位置就在對應(yīng)的角度上。然后在經(jīng)過0度區(qū)域時(可以是-30--30或者其他),對item進行放大縮小操作,可以使用 view.setScaleX()和 view.setScaleY(scale)方法。
4.效果的實現(xiàn)
??4.1 開始自定義LayoutManager
????首先將我們自己的CustomLayoutManager繼承RecyclerView.LayoutManager,實現(xiàn)抽象類RecyclerView.LayoutManager里面的抽象方法generateDefaultLayoutParams(),實現(xiàn)如下
@Overridepublic RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
????當然光重寫上述的方法并沒有什么效果,我們還要重寫LayoutManager
的onLayoutChildren()方法,這個方法從名稱就可以看出來是對子view的一個布局,要實現(xiàn)上面的圖片效果,自然需要對應(yīng)的代碼支持
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//如果沒有item,直接返回
//跳過preLayout,preLayout主要用于支持動畫
if (getItemCount() <= 0 || state.isPreLayout()) {
offsetRotate = 0;
return;
}
//得到子view的寬和高,這邊的item的寬高都是一樣的,所以只需要進行一次測量
View scrap = recycler.getViewForPosition(0);
addView(scrap);
measureChildWithMargins(scrap, 0, 0);
//計算測量布局的寬高
mDecoratedChildWidth = getDecoratedMeasuredWidth(scrap);
mDecoratedChildHeight = getDecoratedMeasuredHeight(scrap);
//確定起始位置,在最上方的中心處
startLeft = (getHorizontalSpace() - mDecoratedChildWidth) / 2;
startTop = 0;
//記錄每個item旋轉(zhuǎn)的角度
float rotate = firstChildRotate;
for (int i = 0; i < getItemCount(); i++) {
itemsRotate.put(i, rotate);
itemAttached.put(i, false);
rotate += intervalAngle;
}
//在布局之前,將所有的子View先Detach掉,放入到Scrap緩存中
detachAndScrapAttachedViews(recycler);
fixRotateOffset();
layoutItems(recycler, state);
}
????從上面的代碼可以看出,因為在這里每個item的大小都是一致的,所以我們只需要測量一次得到item的寬高之后的item就可以直接使用。然后還需要確定最中心的item的位置,這里是startLeft和startTop。然后對每個item進行角度的保存和處理,如下
/**
* 默認每個item之間的角度
**/
private static float INTERVAL_ANGLE = 30f;
/**
* 第一個的角度是為0
**/
private int firstChildRotate = 0;
/**
* 第一個的角度是為0
**/
private int firstChildRotate = 0;
//最大和最小的移除角度
private int minRemoveDegree;
private int maxRemoveDegree;
//記錄Item是否出現(xiàn)過屏幕且還沒有回收。true表示出現(xiàn)過屏幕上,并且還沒被回收
private SparseBooleanArray itemAttached = new SparseBooleanArray();
//保存所有的Item的上下左右的偏移量信息
private SparseArray<Float> itemsRotate = new SparseArray<>();
/**
* 設(shè)置滾動時候的角度
**/
private void fixRotateOffset() {
if (offsetRotate < 0) {
offsetRotate = 0;
}
if (offsetRotate > getMaxOffsetDegree()) {
offsetRotate = getMaxOffsetDegree();
}
}
????上述代碼的作用是對每個item位置的一個記錄和item是否顯示的一個記錄,offsetRotate 保存當前item布局的一個角度總量,將其限制在一個范圍之內(nèi)。在范圍內(nèi)的item用于顯示,之外的進行回收。下面來看layoutItems()方法,該方法是對item的一個回收和顯示。
/**
* 進行view的回收和顯示
**/
private void layoutItems(RecyclerView.Recycler recycler, RecyclerView.State state) {
layoutItems(recycler, state, SCROLL_RIGHT);
}
/**
* 進行view的回收和顯示的具體實現(xiàn)
**/
private void layoutItems(RecyclerView.Recycler recycler,
RecyclerView.State state, int oritention) {
if (state.isPreLayout()) return;
//移除界面之外的view
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
int position = getPosition(view);
if (itemsRotate.get(position) - offsetRotate > maxRemoveDegree
|| itemsRotate.get(position) - offsetRotate < minRemoveDegree) {
itemAttached.put(position, false);
removeAndRecycleView(view, recycler);
}
}
//將要顯示的view進行顯示出來
for (int i = 0; i < getItemCount(); i++) {
if (itemsRotate.get(i) - offsetRotate <= maxRemoveDegree
&& itemsRotate.get(i) - offsetRotate >= minRemoveDegree) {
if (!itemAttached.get(i)) {
View scrap = recycler.getViewForPosition(i);
measureChildWithMargins(scrap, 0, 0);
if (oritention == SCROLL_LEFT)
addView(scrap, 0);
else
addView(scrap);
float rotate = itemsRotate.get(i) - offsetRotate;
int left = calLeftPosition(rotate);
int top = calTopPosition(rotate);
scrap.setRotation(rotate);
layoutDecorated(scrap, startLeft + left, startTop + top,
startLeft + left + mDecoratedChildWidth, startTop + top + mDecoratedChildHeight);
itemAttached.put(i, true);
//計算角度然后進行放大
float scale = calculateScale(rotate);
scrap.setScaleX(scale);
scrap.setScaleY(scale);
}
}
}
}
????首先對于角度在范圍之外的item進行remove掉,對于范圍之內(nèi)的item,通過layoutDecorated()方法進行顯示,根據(jù)左滑還是右滑,判斷item顯示的位置。同時根據(jù)當前item的角度位置,判斷當前item是否需要進行放大或者縮小,計算方法如下。
/**
* 最大放大倍數(shù)
**/
private static final float SCALE_RATE = 1.4f;
//放大的倍數(shù)
private float maxScale;
//在什么角度變化之內(nèi)
private float minScaleRotate = 40;
/**
* 默認每個item之間的角度
**/
private static float INTERVAL_ANGLE = 30f;
/**
* 默認的半徑長度
**/
private static final int DEFAULT_RADIO = 100;
/**
* 半徑默認為100
**/
private int mRadius;
/**
* 根據(jù)角度計算大小,0度的時候最大,minScaleRotate度的時候最小,然后其他時候變小
**/
private float calculateScale(float rotate) {
rotate = Math.abs(rotate) > minScaleRotate ? minScaleRotate : Math.abs(rotate);
return (1 - rotate / minScaleRotate) * (maxScale - 1) + 1;
}
/**
* 當前item的x的坐標
**/
private int calLeftPosition(float rotate) {
return (int) (mRadius * Math.cos(Math.toRadians(90 - rotate)));
}
/**
* 當前item的y的坐標
**/
private int calTopPosition(float rotate) {
return (int) (mRadius * Math.sin(Math.toRadians(90 - rotate)));
}
經(jīng)過以上的步驟,該RecyclerView已經(jīng)能夠進行列表顯示了,但是還不能夠進行滑動。
??4.2 LayoutManager實現(xiàn)滑動
????LayoutManager中還有canScrollHorizontally()和canScrollVertically()分別代表是否可以進行橫向和縱向的滑動。
同時還有scrollHorizontallyBy和scrollVerticallyBy方法來實現(xiàn)對應(yīng)的邏輯。這邊我們需要的是進行橫向的滑動,那么實現(xiàn)方法如下
@Override
public boolean canScrollHorizontally() {
return true;
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
int willScroll = dx;
//每個item x方向上的移動距離
float theta = dx / DISTANCE_RATIO;
float targetRotate = offsetRotate + theta;
//目標角度
if (targetRotate < 0) {
willScroll = (int) (-offsetRotate * DISTANCE_RATIO);
} else if (targetRotate > getMaxOffsetDegree()) {
willScroll = (int) ((getMaxOffsetDegree() - offsetRotate) * DISTANCE_RATIO);
}
theta = willScroll / DISTANCE_RATIO;
//當前移動的總角度
offsetRotate += theta;
//重新設(shè)置每個item的x和y的坐標
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
float newRotate = view.getRotation() - theta;
int offsetX = calLeftPosition(newRotate);
int offsetY = calTopPosition(newRotate);
layoutDecorated(view, startLeft + offsetX, startTop + offsetY,
startLeft + offsetX + mDecoratedChildWidth, startTop + offsetY + mDecoratedChildHeight);
view.setRotation(newRotate);
//計算角度然后進行放大
float scale = calculateScale(newRotate);
view.setScaleX(scale);
view.setScaleY(scale);
}
//根據(jù)dx的大小判斷是左滑還是右滑
if (dx < 0)
layoutItems(recycler, state, SCROLL_LEFT);
else
layoutItems(recycler, state, SCROLL_RIGHT);
return willScroll;
}
????可以看出來scrollHorizontallyBy()里面的操作類似與layoutItem()方法中的操作,只不過在這里需要對橫向滾動量dx進行角度的裝換,這邊給的比例為DISTANCE_RATIO,然后需要對offsetRotate進行累加操作,最后調(diào)用layoutItems進行item的view的回收和顯示。經(jīng)過以上的操作,RecyclerView就可以進行列表的展示和滑動了。
?? 4.3 RecyclerView的自行滾動到某一項
????需要注意的是,由于我們已經(jīng)修改了LayoutManager的滾動規(guī)則。
/**
* Convenience method to scroll to a certain position.
*
* RecyclerView does not implement scrolling logic, rather forwards the call to
* {@link android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)}
* @param position Scroll to this adapter position
* @see android.support.v7.widget.RecyclerView.LayoutManager#scrollToPosition(int)
*/
public void scrollToPosition(int position) {
if (mLayoutFrozen) {
return;
}
stopScroll();
if (mLayout == null) {
Log.e(TAG, "Cannot scroll to position a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
return;
}
mLayout.scrollToPosition(position);
awakenScrollBars();
}
/**
* Starts a smooth scroll to an adapter position.
* <p>
* To support smooth scrolling, you must override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} and create a
* {@link SmoothScroller}.
* <p>
* {@link LayoutManager} is responsible for creating the actual scroll action. If you want to
* provide a custom smooth scroll logic, override
* {@link LayoutManager#smoothScrollToPosition(RecyclerView, State, int)} in your
* LayoutManager.
*
* @param position The adapter position to scroll to
* @see LayoutManager#smoothScrollToPosition(RecyclerView, State, int)
*/
public void smoothScrollToPosition(int position) {
if (mLayoutFrozen) {
return;
}
if (mLayout == null) {
Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
return;
}
mLayout.smoothScrollToPosition(this, mState, position);
}
????而從源碼中可以看出來RecyclerView的scrollToPosition()(滾動到某一項),
smoothScrollToPosition()(平滑滾動到某一項)其實是調(diào)用的layoutManager中對于的方法,所以我們必須重寫我們自己的layoutManager的對于的方法,實現(xiàn)自己的滾動規(guī)則。
private PointF computeScrollVectorForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
final int firstChildPos = getPosition(getChildAt(0));
final int direction = targetPosition < firstChildPos ? -1 : 1;
return new PointF(direction, 0);
}
@Override
public void scrollToPosition(int position) {//移動到某一項
if (position < 0 || position > getItemCount() - 1) return;
float targetRotate = position * intervalAngle;
if (targetRotate == offsetRotate) return;
offsetRotate = targetRotate;
fixRotateOffset();
requestLayout();
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {//平滑的移動到某一項
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return CustomLayoutManager.this.computeScrollVectorForPosition(targetPosition);
}
};
smoothScroller.setTargetPosition(position);
startSmoothScroll(smoothScroller);
}
????在scrollToPosition中,我們需要根據(jù)position獲取對應(yīng)的item在最開始布局時候的角度,賦值給offsetRotate ,讓其進行重新布局即可以實現(xiàn)效果。而在smoothScrollToPosition()中,參考LinearLayoumanager中對應(yīng)的方法。
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext());
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
if (getChildCount() == 0) {
return null;
}
final int firstChildPos = getPosition(getChildAt(0));
final int direction = targetPosition < firstChildPos != mShouldReverseLayout ? -1 : 1;
if (mOrientation == HORIZONTAL) {
return new PointF(direction, 0);
} else {
return new PointF(0, direction);
}
}
????從computeScrollVectorForPosition可以知道,對于我們自己的LayoutManager而言
從前面的item滾動到后面的是new PointF(-1, 0);相反的就是new PointF(1, 0);最后我們重寫onAdapterChanged()方法,當adapter發(fā)生變化的時候,讓我們自己的layoutmanager回歸原始的狀態(tài)。
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {//adapter進行改變的時候
removeAllViews();
offsetRotate = 0;
}
5.最后
????只要了解了RecyclerView的機制和RecyclerView.LayoutManager中需要實現(xiàn)的方法和步奏,那么對于我們自己來說,打造自己的LayoutManager并不是一件很難的事情。我在實現(xiàn)的過程中,主要參考了
csdn huachao1001的文章http://blog.csdn.net/huachao1001/article/details/51594004 和
簡書Dajavu的文章http://www.lxweimin.com/p/7bb7556bbe10 ,最終實現(xiàn)上面的效果,
最后奉上Github上代碼下載地址https://github.com/hzl123456/CustomLayoutManager