不少app的首頁都有一個能夠滑動的banner。大概長這樣:java
實現方式有多種,介紹一種用RecyclerView的實現方式git
第一種平鋪的banner 其實很好實現,就是一個RecycerView + PagerSnapHelper。可是爲了兼容多種顯示效果,例如第二種的顯示效果,咱們須要去自定義LayoutMmanager和SnapHelper。github
自定義LayoutManager通常是分兩步,佈局和滑動,再想一想LayoutManager須要些什麼屬性。bash
須要一個boolean值標識是否循環佈局。 須要兩個float值標識滑動時的寬高縮放。app
public class BannerLayoutManager{
private float heightScale = 0.9f;
private float widthScale = 0.9f;
private boolean infinite = true; //默認無限循環
...
}
複製代碼
一、計算第一個View的開始位置 : int offsetX = (父佈局寬度 - 子View寬度) / 2ide
int offsetX = (mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(scrap)) / 2;
複製代碼
二、計算是否要添加一個view爲第1個子view,以顯示出循環佈局的效果。佈局
View lastChild = getChildAt(getChildCount() - 1);
// 若是是循環佈局,而且最後一個view已超出父佈局,則添加最左邊的view
if ( infinite && lastChild != null && getDecoratedRight(lastChild) > mOrientationHelper.getTotalSpace()) {
layoutLeftItem(recycler);
}
複製代碼
三、縮放全部的view
縮放規則:以父佈局的中線(中心線)爲基準,若是子view的中線與中心線重合,則縮放比爲1.0f;若是不重合,則計算出子view的中線與中心線的距離,距離越大,縮放比越小。動畫
private void scaleItem() {
if (heightScale >= 1 || widthScale >= 1) {
return;
}
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
float itemMiddle = (getDecoratedRight(child) + getDecoratedLeft(child)) / 2.0f;
float screenMiddle = mOrientationHelper.getTotalSpace() / 2.0f;
float interval = Math.abs(screenMiddle - itemMiddle) * 1.0f;
float ratio = 1 - (1 - heightScale) * (interval / itemWidth);
float ratioWidth = 1 - (1 - widthScale) * (interval / itemWidth);
child.setScaleX(ratioWidth);
child.setScaleY(ratio);
}
}
複製代碼
四、整體的佈局方法ui
private void layoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getItemCount() == 0 || state.isPreLayout()) {
removeAndRecycleAllViews(recycler);
return;
}
detachAndScrapAttachedViews(recycler);
View scrap = recycler.getViewForPosition(0);
measureChildWithMargins(scrap, 0, 0);
itemWidth = getDecoratedMeasuredWidth(scrap);
int offsetX = (mOrientationHelper.getTotalSpace() - mOrientationHelper.getDecoratedMeasurement(scrap)) / 2;
for (int i = 0; i < getItemCount(); i++) {
if (offsetX > mOrientationHelper.getTotalSpace()) {
break;
}
View viewForPosition = recycler.getViewForPosition(i);
addView(viewForPosition);
measureChildWithMargins(viewForPosition, 0, 0);
offsetX += layoutItem(viewForPosition, offsetX);
}
View lastChild = getChildAt(getChildCount() - 1);
// 若是是循環佈局,而且最後一個view已超出父佈局,則添加最左邊的view
if ( infinite && lastChild != null && getDecoratedRight(lastChild) > mOrientationHelper.getTotalSpace()) {
layoutLeftItem(recycler);
}
scaleItem();
}
複製代碼
對滑動的處理就是爲了對view的回收,以減小消耗,提升效率。 處理方式就是根據滑動距離去添加和刪除view。this
我也是第一次自定義LayoutManager,感受寫得有點繁瑣了。分了左滑右滑兩種狀況去寫。
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
return offsetDx(dx, recycler);
}
private int offsetDx(int dx, RecyclerView.Recycler recycler) {
int realScroll = dx;
// 向左
if (dx > 0) {
realScroll = scrollToLeft(dx, recycler, realScroll);
}
// 向右
if (dx < 0) {
realScroll = scrollToRight(dx, recycler, realScroll);
}
scaleItem();
return realScroll;
}
複製代碼
scrollToLeft或者scrollToRight都是隻作了三件事,添加view,計算實際滑動距離並滑動,回收view
private int scrollToLeft(int dx, RecyclerView.Recycler recycler, int realScroll) {
while (true) {
// 將須要添加的view添加到RecyclerView中
View rightView = getChildAt(getChildCount() - 1);
int decoratedRight = getDecoratedRight(rightView);
if (decoratedRight - dx < mOrientationHelper.getTotalSpace()) {
int position = getPosition(rightView);
if (!infinite && position == getItemCount() - 1) {
break;
}
int addPosition = infinite ? (position + 1) % getItemCount() : position + 1;
View lastViewAdd = recycler.getViewForPosition(addPosition);
addView(lastViewAdd);
measureChildWithMargins(lastViewAdd, 0, 0);
int left = decoratedRight;
layoutDecoratedWithMargins(lastViewAdd, left, getItemTop(lastViewAdd), left + getDecoratedMeasuredWidth(lastViewAdd), getItemTop(lastViewAdd) + getDecoratedMeasuredHeight(lastViewAdd));
} else {
break;
}
}
// 處理滑動
View lastChild = getChildAt(getChildCount() - 1);
int left = getDecoratedLeft(lastChild);
if (getPosition(lastChild) == getItemCount() - 1) {
// 最後一個view已經到底了,計算實際能夠滑動的距離
if (left - dx < 0) {
realScroll = left;
}
}
offsetChildrenHorizontal(-realScroll);
// 回收滑出父佈局的view
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
int decoratedRight = getDecoratedRight(child);
if (decoratedRight < 0) {
removeAndRecycleView(child, recycler);
}
}
return realScroll;
}
複製代碼
這樣,自定義的LayoutManager基本就完成了。
自定義完成了LayoutManager的確能夠高效的實現gif中的效果,可是滑動的時候就有問題了,RecyclerView默認是支持fling操做的,就是慣性滑動。而沒法作到一次只滑動一頁,而且居中顯示的效果(相似ViewPager的滑動效果)。
爲了實現這種效果,google提供了一個SnapHelper抽象類,咱們能夠繼承這個去實現本身的滑動邏輯。SDK提供了PagerSnapHelper和LinearSnapHelper兩種實現。
PagerSnapHelper能夠作到ViewPager那種一次滑動一頁的效果,可是當滑動到最後一個view的時候會明顯的出現卡頓。由於PagerSnapHelper默認不支持循環佈局這種狀況的。因此我繼承PagerSnaperHelper,修改了一點點邏輯,實現了循環滑動的效果。
public class BannerPageSnapHelper extends PagerSnapHelper {
private boolean infinite = false;
private OrientationHelper horizontalHelper;
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX,
int velocityY) {
final int itemCount = layoutManager.getItemCount();
if (itemCount == 0) {
return RecyclerView.NO_POSITION;
}
View mStartMostChildView = findStartView(layoutManager, getHorizontalHelper(layoutManager));
if (mStartMostChildView == null) {
return RecyclerView.NO_POSITION;
}
final int centerPosition = layoutManager.getPosition(mStartMostChildView);
if (centerPosition == RecyclerView.NO_POSITION) {
return RecyclerView.NO_POSITION;
}
final boolean forwardDirection;
if (layoutManager.canScrollHorizontally()) {
forwardDirection = velocityX > 0;
} else {
forwardDirection = velocityY > 0;
}
if (forwardDirection) {
if (centerPosition == layoutManager.getItemCount() - 1) {
return infinite ? 0 : layoutManager.getItemCount() - 1;
} else {
return centerPosition + 1;
}
} else {
return centerPosition;
}
}
private View findStartView(RecyclerView.LayoutManager layoutManager,
OrientationHelper helper) {
int childCount = layoutManager.getChildCount();
if (childCount == 0) {
return null;
}
View closestChild = null;
int start = Integer.MAX_VALUE;
for (int i = 0; i < childCount; i++) {
final View child = layoutManager.getChildAt(i);
int childStart = helper.getDecoratedStart(child);
/** if child is more to start than previous closest, set it as closest **/
if (childStart < start) {
start = childStart;
closestChild = child;
}
}
return closestChild;
}
@NonNull
private OrientationHelper getHorizontalHelper(
@NonNull RecyclerView.LayoutManager layoutManager) {
if (horizontalHelper == null) {
horizontalHelper = OrientationHelper.createHorizontalHelper(layoutManager);
}
return horizontalHelper;
}
public boolean isInfinite() {
return infinite;
}
public void setInfinite(boolean infinite) {
this.infinite = infinite;
}
}
複製代碼
關於banner部分,通常項目會有如下幾個參數。
一、style 展現樣式:例如圓角 或是平鋪。 能夠在每一個子view外面套一個CardView 去設置圓角,而後根據需求在adapter中設置view的寬高。
二、是否循環顯示:BannerLayoutManager和PagerHelper都有一個屬性,infinite,爲true時,循環顯示。
三、自動播放:這個在Activity或者Fragment中用Rxjava或者Handler加一個定時器,調用 recyclerView.smoothScrollToPosition(position)就好了 。
四、滑動動畫的顯示時間:BannerLayoutManager中有個smoothScrollTime屬性,調用set方法設置一下就好了。
應該能知足大多數需求吧。。
想了解詳情去看代碼吧 github.com/ZhangHao555…