先上效果图
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/b5b03fee7430369df3b3f0fcd0f43cc6.gif)
布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.xuexiang.xui.widget.banner.recycler.BannerLayout
android:id="@+id/bl_horizontal"
android:layout_width="match_parent"
android:layout_height="200dp"
app:bl_autoPlaying="true"
app:bl_centerScale="1.3"
app:bl_itemSpace="10dp"
app:bl_moveSpeed="1.8" />
<com.xuexiang.xui.widget.banner.recycler.BannerLayout
android:id="@+id/bl_vertical"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="10dp"
app:bl_orientation="vertical"
app:bl_showIndicator="false" />
</LinearLayout>
RecyclerViewBannerFragment
public class RecyclerViewBannerFragment extends BaseFragment implements BannerLayout.OnBannerItemClickListener {
@BindView(R.id.bl_horizontal)
BannerLayout blHorizontal;
@BindView(R.id.bl_vertical)
BannerLayout blVertical;
private RecyclerViewBannerAdapter mAdapterHorizontal;
private RecyclerViewBannerAdapter mAdapterVertical;
/**
* 布局的资源id
*
* @return
*/
@Override
protected int getLayoutId() {
return R.layout.fragment_recyclerview_banner;
}
/**
* 初始化控件
*/
@Override
protected void initViews() {
blHorizontal.setAdapter(mAdapterHorizontal = new RecyclerViewBannerAdapter(DemoDataProvider.urls));
blVertical.setAdapter(mAdapterVertical = new RecyclerViewBannerAdapter(DemoDataProvider.urls));
}
@Override
protected void initListeners() {
mAdapterHorizontal.setOnBannerItemClickListener(this);
mAdapterVertical.setOnBannerItemClickListener(this);
blHorizontal.setOnIndicatorIndexChangedListener(new BannerLayout.OnIndicatorIndexChangedListener() {
@Override
public void onIndexChanged(int position) {
XToastUtils.toast("轮播到第" + (position + 1) + "个");
}
});
}
@Override
public void onItemClick(int position) {
XToastUtils.toast("点击了第" + (position + 1) + "个");
}
}
BannerLayout
package com.xuexiang.xui.widget.banner.recycler;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.LayerDrawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.core.view.GravityCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.xuexiang.xui.R;
import com.xuexiang.xui.utils.ResUtils;
import com.xuexiang.xui.widget.banner.recycler.layout.BannerLayoutManager;
import com.xuexiang.xui.widget.banner.recycler.layout.CenterSnapHelper;
import static android.widget.AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
/**
* 使用RecyclerView实现轮播Banner
*
* @author lk
*/
public class BannerLayout extends FrameLayout {
private final static int WHAT_AUTO_PLAY = 1000;
private RecyclerView mIndicatorContainer;
private IndicatorAdapter mIndicatorAdapter;
private int mIndicatorMargin;//指示器间距
private Drawable mSelectedDrawable;
private Drawable mUnselectedDrawable;
private RecyclerView mRecyclerView;
private BannerLayoutManager mLayoutManager;
private boolean mHasInit;
private int mBannerSize = 1;
private int mCurrentIndex;
private boolean mIsPlaying = false;
private boolean mIsAutoPlaying = true;
private int mAutoPlayDuration;//刷新间隔时间
private boolean mShowIndicator;//是否显示指示器
private int mItemSpace;
private float mCenterScale;
private float mMoveSpeed;
private OnIndicatorIndexChangedListener mOnIndicatorIndexChangedListener;
protected Handler mHandler = new Handler(new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
if (msg.what == WHAT_AUTO_PLAY) {
if (mCurrentIndex == mLayoutManager.getCurrentPosition()) {
++mCurrentIndex;
mRecyclerView.smoothScrollToPosition(mCurrentIndex);
mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, mAutoPlayDuration);
refreshIndicator();
}
}
return false;
}
});
public BannerLayout(Context context) {
this(context, null);
}
public BannerLayout(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.BannerLayoutStyle);
}
public BannerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs, defStyleAttr);
}
protected void initView(Context context, AttributeSet attrs, int defStyleAttr) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.BannerLayout, defStyleAttr, 0);
mShowIndicator = typedArray.getBoolean(R.styleable.BannerLayout_bl_showIndicator, true);
mAutoPlayDuration = typedArray.getInt(R.styleable.BannerLayout_bl_interval, 4000);
mIsAutoPlaying = typedArray.getBoolean(R.styleable.BannerLayout_bl_autoPlaying, true);
mItemSpace = typedArray.getDimensionPixelSize(R.styleable.BannerLayout_bl_itemSpace, ResUtils.getDimensionPixelSize(R.dimen.default_recycler_banner_itemSpace));
mCenterScale = typedArray.getFloat(R.styleable.BannerLayout_bl_centerScale, 1.2F);
mMoveSpeed = typedArray.getFloat(R.styleable.BannerLayout_bl_moveSpeed, 1.0F);
mSelectedDrawable = ResUtils.getDrawableAttrRes(getContext(), typedArray, R.styleable.BannerLayout_bl_indicatorSelectedSrc);
mUnselectedDrawable = ResUtils.getDrawableAttrRes(getContext(), typedArray, R.styleable.BannerLayout_bl_indicatorUnselectedSrc);
int indicatorSize = typedArray.getDimensionPixelSize(R.styleable.BannerLayout_bl_indicatorSize, ResUtils.getDimensionPixelSize(R.dimen.default_recycler_banner_indicatorSize));
int indicatorSelectedColor = typedArray.getColor(R.styleable.BannerLayout_bl_indicatorSelectedColor, ResUtils.getColor(R.color.xui_config_color_red));
int indicatorUnselectedColor = typedArray.getColor(R.styleable.BannerLayout_bl_indicatorUnselectedColor, ResUtils.getColor(R.color.xui_config_color_gray_2));
if (mSelectedDrawable == null) {
//绘制默认选中状态图形
GradientDrawable selectedGradientDrawable = new GradientDrawable();
selectedGradientDrawable.setShape(GradientDrawable.OVAL);
selectedGradientDrawable.setColor(indicatorSelectedColor);
selectedGradientDrawable.setSize(indicatorSize, indicatorSize);
selectedGradientDrawable.setCornerRadius(indicatorSize >> 1);
mSelectedDrawable = new LayerDrawable(new Drawable[]{selectedGradientDrawable});
}
if (mUnselectedDrawable == null) {
//绘制默认未选中状态图形
GradientDrawable unSelectedGradientDrawable = new GradientDrawable();
unSelectedGradientDrawable.setShape(GradientDrawable.OVAL);
unSelectedGradientDrawable.setColor(indicatorUnselectedColor);
unSelectedGradientDrawable.setSize(indicatorSize, indicatorSize);
unSelectedGradientDrawable.setCornerRadius(indicatorSize >> 1);
mUnselectedDrawable = new LayerDrawable(new Drawable[]{unSelectedGradientDrawable});
}
mIndicatorMargin = typedArray.getDimensionPixelSize(R.styleable.BannerLayout_bl_indicatorSpace, ResUtils.getDimensionPixelSize(R.dimen.default_recycler_banner_indicatorSpace));
int marginLeft = typedArray.getDimensionPixelSize(R.styleable.BannerLayout_bl_indicatorMarginLeft, ResUtils.getDimensionPixelSize(R.dimen.default_recycler_banner_indicatorMarginLeft));
int marginRight = typedArray.getDimensionPixelSize(R.styleable.BannerLayout_bl_indicatorMarginRight, ResUtils.getDimensionPixelSize(R.dimen.default_recycler_banner_indicatorMarginRight));
int marginBottom = typedArray.getDimensionPixelSize(R.styleable.BannerLayout_bl_indicatorMarginBottom, ResUtils.getDimensionPixelSize(R.dimen.default_recycler_banner_indicatorMarginBottom));
int g = typedArray.getInt(R.styleable.BannerLayout_bl_indicatorGravity, 0);
int gravity;
if (g == 0) {
gravity = GravityCompat.START;
} else if (g == 2) {
gravity = GravityCompat.END;
} else {
gravity = Gravity.CENTER;
}
int orientation = typedArray.getInt(R.styleable.BannerLayout_bl_orientation, OrientationHelper.HORIZONTAL);
typedArray.recycle();
//轮播图部分
mRecyclerView = new RecyclerView(context);
LayoutParams vpLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
addView(mRecyclerView, vpLayoutParams);
mLayoutManager = new BannerLayoutManager(getContext(), orientation);
mLayoutManager.setItemSpace(mItemSpace);
mLayoutManager.setCenterScale(mCenterScale);
mLayoutManager.setMoveSpeed(mMoveSpeed);
mRecyclerView.setLayoutManager(mLayoutManager);
new CenterSnapHelper().attachToRecyclerView(mRecyclerView);
//指示器部分
mIndicatorContainer = new RecyclerView(context);
LinearLayoutManager indicatorLayoutManager = new LinearLayoutManager(context, orientation, false);
mIndicatorContainer.setLayoutManager(indicatorLayoutManager);
mIndicatorAdapter = new IndicatorAdapter();
mIndicatorContainer.setAdapter(mIndicatorAdapter);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.BOTTOM | gravity;
params.setMargins(marginLeft, 0, marginRight, marginBottom);
addView(mIndicatorContainer, params);
if (!mShowIndicator) {
mIndicatorContainer.setVisibility(GONE);
}
}
// 设置是否禁止滚动播放
public BannerLayout setAutoPlaying(boolean isAutoPlaying) {
mIsAutoPlaying = isAutoPlaying;
setPlaying(this.mIsAutoPlaying);
return this;
}
public boolean isPlaying() {
return mIsPlaying;
}
//设置是否显示指示器
public BannerLayout setShowIndicator(boolean showIndicator) {
mShowIndicator = showIndicator;
mIndicatorContainer.setVisibility(showIndicator ? VISIBLE : GONE);
return this;
}
//设置当前图片缩放系数
public BannerLayout setCenterScale(float centerScale) {
mCenterScale = centerScale;
mLayoutManager.setCenterScale(centerScale);
return this;
}
//设置跟随手指的移动速度
public BannerLayout setMoveSpeed(float moveSpeed) {
mMoveSpeed = moveSpeed;
mLayoutManager.setMoveSpeed(moveSpeed);
return this;
}
//设置图片间距
public BannerLayout setItemSpace(int itemSpace) {
mItemSpace = itemSpace;
mLayoutManager.setItemSpace(itemSpace);
return this;
}
/**
* 设置轮播间隔时间
*
* @param autoPlayDuration 时间毫秒
*/
public BannerLayout setAutoPlayDuration(int autoPlayDuration) {
mAutoPlayDuration = autoPlayDuration;
return this;
}
public BannerLayout setOrientation(int orientation) {
mLayoutManager.setOrientation(orientation);
return this;
}
/**
* 设置是否自动播放(上锁)
*
* @param playing 开始播放
*/
protected synchronized void setPlaying(boolean playing) {
if (mIsAutoPlaying && mHasInit) {
if (!mIsPlaying && playing) {
mHandler.sendEmptyMessageDelayed(WHAT_AUTO_PLAY, mAutoPlayDuration);
mIsPlaying = true;
} else if (mIsPlaying && !playing) {
mHandler.removeMessages(WHAT_AUTO_PLAY);
mIsPlaying = false;
}
}
}
/**
* 设置轮播数据集
*/
public void setAdapter(RecyclerView.Adapter adapter) {
mHasInit = false;
mRecyclerView.setAdapter(adapter);
mBannerSize = adapter.getItemCount();
mLayoutManager.setInfinite(mBannerSize >= 3);
setPlaying(true);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0) {
setPlaying(false);
}
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
int first = mLayoutManager.getCurrentPosition();
if (mCurrentIndex != first) {
mCurrentIndex = first;
}
if (newState == SCROLL_STATE_IDLE) {
setPlaying(true);
}
refreshIndicator();
}
});
mHasInit = true;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
setPlaying(false);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setPlaying(true);
break;
}
return super.dispatchTouchEvent(ev);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
setPlaying(true);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
setPlaying(false);
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == VISIBLE) {
setPlaying(true);
} else {
setPlaying(false);
}
}
/**
* 标示点适配器
*/
protected class IndicatorAdapter extends RecyclerView.Adapter {
int currentPosition = 0;
public void setPosition(int currentPosition) {
this.currentPosition = currentPosition;
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ImageView bannerPoint = new ImageView(getContext());
RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
lp.setMargins(mIndicatorMargin, mIndicatorMargin, mIndicatorMargin, mIndicatorMargin);
bannerPoint.setLayoutParams(lp);
return new RecyclerView.ViewHolder(bannerPoint) {
};
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ImageView bannerPoint = (ImageView) holder.itemView;
bannerPoint.setImageDrawable(currentPosition == position ? mSelectedDrawable : mUnselectedDrawable);
}
@Override
public int getItemCount() {
return mBannerSize;
}
}
/**
* 改变导航的指示点
*/
protected synchronized void refreshIndicator() {
if (mBannerSize > 1) {
final int position = mCurrentIndex % mBannerSize;
if (mShowIndicator) {
mIndicatorAdapter.setPosition(position);
mIndicatorAdapter.notifyDataSetChanged();
}
if (mOnIndicatorIndexChangedListener != null) {
mOnIndicatorIndexChangedListener.onIndexChanged(position);
}
}
}
public interface OnBannerItemClickListener {
void onItemClick(int position);
}
public BannerLayout setOnIndicatorIndexChangedListener(OnIndicatorIndexChangedListener onIndicatorIndexChangedListener) {
mOnIndicatorIndexChangedListener = onIndicatorIndexChangedListener;
return this;
}
/**
* 索引变化监听
*/
public interface OnIndicatorIndexChangedListener {
/**
* 索引变化
*
* @param position
*/
void onIndexChanged(int position);
}
}
BannerLayoutManager
package com.xuexiang.xui.widget.banner.recycler.layout;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.OrientationHelper;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
/**
* An implementation of {@link RecyclerView.LayoutManager} which behaves like view pager.
* Please make sure your child view have the same size.
*/
public class BannerLayoutManager extends RecyclerView.LayoutManager {
public static final int DETERMINE_BY_MAX_AND_MIN = -1;
public static final int HORIZONTAL = OrientationHelper.HORIZONTAL;
public static final int VERTICAL = OrientationHelper.VERTICAL;
private static final int DIRECTION_NO_WHERE = -1;
private static final int DIRECTION_FORWARD = 0;
private static final int DIRECTION_BACKWARD = 1;
protected static final int INVALID_SIZE = Integer.MAX_VALUE;
private SparseArray<View> positionCache = new SparseArray<>();
protected int mDecoratedMeasurement;
protected int mDecoratedMeasurementInOther;
/**
* Current orientation. Either {@link #HORIZONTAL} or {@link #VERTICAL}
*/
int mOrientation;
protected int mSpaceMain;
protected int mSpaceInOther;
/**
* The offset of property which will change while scrolling
*/
protected float mOffset;
/**
* Many calculations are made depending on orientation. To keep it clean, this interface
* helps {@link LinearLayoutManager} make those decisions.
* Based on {@link #mOrientation}, an implementation is lazily created in
* {@link #ensureLayoutState} method.
*/
protected OrientationHelper mOrientationHelper;
/**
* Defines if layout should be calculated from end to start.
*/
private boolean mReverseLayout = false;
/**
* This keeps the final value for how LayoutManager should start laying out views.
* It is calculated by checking {@link #getReverseLayout()} and View's layout direction.
* {@link #onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)} is run.
*/
private boolean mShouldReverseLayout = false;
/**
* Works the same way as {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}.
* see {@link android.widget.AbsListView#setSmoothScrollbarEnabled(boolean)}
*/
private boolean mSmoothScrollbarEnabled = true;
/**
* When LayoutManager needs to scroll to a position, it sets this variable and requests a
* layout which will check this variable and re-layout accordingly.
*/
private int mPendingScrollPosition = NO_POSITION;
private SavedState mPendingSavedState = null;
protected float mInterval; //the mInterval of each item's mOffset
/* package */ OnPageChangeListener onPageChangeListener;
private boolean mRecycleChildrenOnDetach;
private boolean mInfinite = true;
private boolean mEnableBringCenterToFront;
private int mLeftItems;
private int mRightItems;
/**
* max visible item count
*/
private int mMaxVisibleItemCount = DETERMINE_BY_MAX_AND_MIN;
private Interpolator mSmoothScrollInterpolator;
private int mDistanceToBottom = INVALID_SIZE;
/**
* use for handle focus
*/
private View currentFocusView;
/**
* @return the mInterval of each item's mOffset
*/
/**
* @return the mInterval of each item's mOffset
*/
private int itemSpace = 20;
private float centerScale = 1.2f;
private float moveSpeed = 1.0f;
protected float getDistanceRatio() {
if (moveSpeed == 0) return Float.MAX_VALUE;
return 1 / moveSpeed;
}
protected float setInterval() {
return mDecoratedMeasurement * ((centerScale - 1) / 2 + 1) + itemSpace;
}
public void setItemSpace(int itemSpace) {
this.itemSpace = itemSpace;
}
public void setCenterScale(float centerScale) {
this.centerScale = centerScale;
}
public void setMoveSpeed(float moveSpeed) {
assertNotInLayoutOrScroll(null);
if (this.moveSpeed == moveSpeed) return;
this.moveSpeed = moveSpeed;
}
protected void setItemViewProperty(View itemView, float targetOffset) {
float scale = calculateScale(targetOffset + mSpaceMain);
itemView.setScaleX(scale);
itemView.setScaleY(scale);
}
/**
* @param x start positon of the view you want scale
* @return the scale rate of current scroll mOffset
*/
private float calculateScale(float x) {
float deltaX = Math.abs(x - (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2f);
float diff = 0f;
if ((mDecoratedMeasurement - deltaX) > 0) diff = mDecoratedMeasurement - deltaX;
return (centerScale - 1f) / mDecoratedMeasurement * diff + 1;
}
/**
* cause elevation is not support below api 21,
* so you can set your elevation here for supporting it below api 21
* or you can just setElevation in {@link #setItemViewProperty(View, float)}
*/
protected float setViewElevation(View itemView, float targetOffset) {
return 0;
}
/**
* Creates a horizontal ViewPagerLayoutManager
*/
public BannerLayoutManager(Context context) {
this(context, HORIZONTAL, false);
}
/**
* @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public BannerLayoutManager(Context context, int orientation) {
this(context, orientation, false);
}
public BannerLayoutManager(Context context, int orientation, boolean reverseLayout) {
setEnableBringCenterToFront(true);
setMaxVisibleItemCount(3);
setOrientation(orientation);
setReverseLayout(reverseLayout);
setAutoMeasureEnabled(true);
setItemPrefetchEnabled(false);
}
@Override
public RecyclerView.LayoutParams generateDefaultLayoutParams() {
return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT);
}
/**
* Returns whether LayoutManager will recycle its children when it is detached from
* RecyclerView.
*
* @return true if LayoutManager will recycle its children when it is detached from
* RecyclerView.
*/
public boolean getRecycleChildrenOnDetach() {
return mRecycleChildrenOnDetach;
}
/**
* Set whether LayoutManager will recycle its children when it is detached from
* RecyclerView.
* <p>
* If you are using a {@link RecyclerView.RecycledViewPool}, it might be a good idea to set
* this flag to <code>true</code> so that views will be available to other RecyclerViews
* immediately.
* <p>
* Note that, setting this flag will result in a performance drop if RecyclerView
* is restored.
*
* @param recycleChildrenOnDetach Whether children should be recycled in detach or not.
*/
public void setRecycleChildrenOnDetach(boolean recycleChildrenOnDetach) {
mRecycleChildrenOnDetach = recycleChildrenOnDetach;
}
@Override
public void onDetachedFromWindow(RecyclerView view, RecyclerView.Recycler recycler) {
super.onDetachedFromWindow(view, recycler);
if (mRecycleChildrenOnDetach) {
removeAndRecycleAllViews(recycler);
recycler.clear();
}
}
@Override
public Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
return new SavedState(mPendingSavedState);
}
SavedState savedState = new SavedState();
savedState.position = mPendingScrollPosition;
savedState.offset = mOffset;
savedState.isReverseLayout = mShouldReverseLayout;
return savedState;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof SavedState) {
mPendingSavedState = new SavedState((SavedState) state);
requestLayout();
}
}
/**
* @return true if {@link #getOrientation()} is {@link #HORIZONTAL}
*/
@Override
public boolean canScrollHorizontally() {
return mOrientation == HORIZONTAL;
}
/**
* @return true if {@link #getOrientation()} is {@link #VERTICAL}
*/
@Override
public boolean canScrollVertically() {
return mOrientation == VERTICAL;
}
/**
* Returns the current orientation of the layout.
*
* @return Current orientation, either {@link #HORIZONTAL} or {@link #VERTICAL}
* @see #setOrientation(int)
*/
public int getOrientation() {
return mOrientation;
}
/**
* Sets the orientation of the layout. {@link BannerLayoutManager}
* will do its best to keep scroll position.
*
* @param orientation {@link #HORIZONTAL} or {@link #VERTICAL}
*/
public void setOrientation(int orientation) {
if (orientation != HORIZONTAL && orientation != VERTICAL) {
throw new IllegalArgumentException("invalid orientation:" + orientation);
}
assertNotInLayoutOrScroll(null);
if (orientation == mOrientation) {
return;
}
mOrientation = orientation;
mOrientationHelper = null;
mDistanceToBottom = INVALID_SIZE;
removeAllViews();
}
/**
* Returns the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
* And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
*
* @return Max visible item count
*/
public int getMaxVisibleItemCount() {
return mMaxVisibleItemCount;
}
/**
* Set the max visible item count, {@link #DETERMINE_BY_MAX_AND_MIN} means it haven't been set now
* And it will use {@link #maxRemoveOffset()} and {@link #minRemoveOffset()} to handle the range
*
* @param mMaxVisibleItemCount Max visible item count
*/
public void setMaxVisibleItemCount(int mMaxVisibleItemCount) {
assertNotInLayoutOrScroll(null);
if (this.mMaxVisibleItemCount == mMaxVisibleItemCount) return;
this.mMaxVisibleItemCount = mMaxVisibleItemCount;
removeAllViews();
}
/**
* Calculates the view layout order. (e.g. from end to start or start to end)
* RTL layout support is applied automatically. So if layout is RTL and
* {@link #getReverseLayout()} is {@code true}, elements will be laid out starting from left.
*/
private void resolveShouldLayoutReverse() {
if (mOrientation == HORIZONTAL && getLayoutDirection() == ViewCompat.LAYOUT_DIRECTION_RTL) {
mReverseLayout = !mReverseLayout;
}
}
/**
* Returns if views are laid out from the opposite direction of the layout.
*
* @return If layout is reversed or not.
* @see #setReverseLayout(boolean)
*/
public boolean getReverseLayout() {
return mReverseLayout;
}
/**
* Used to reverse item traversal and layout order.
* This behaves similar to the layout change for RTL views. When set to true, first item is
* laid out at the end of the UI, second item is laid out before it etc.
* <p>
* For horizontal layouts, it depends on the layout direction.
* When set to true, If {@link androidx.recyclerview.widget.RecyclerView} is LTR, than it will
* layout from RTL, if {@link androidx.recyclerview.widget.RecyclerView}} is RTL, it will layout
* from LTR.
*/
public void setReverseLayout(boolean reverseLayout) {
assertNotInLayoutOrScroll(null);
if (reverseLayout == mReverseLayout) {
return;
}
mReverseLayout = reverseLayout;
removeAllViews();
}
public void setSmoothScrollInterpolator(Interpolator smoothScrollInterpolator) {
this.mSmoothScrollInterpolator = smoothScrollInterpolator;
}
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
final int offsetPosition = getOffsetToPosition(position);
if (mOrientation == VERTICAL) {
recyclerView.smoothScrollBy(0, offsetPosition, mSmoothScrollInterpolator);
} else {
recyclerView.smoothScrollBy(offsetPosition, 0, mSmoothScrollInterpolator);
}
}
@Override
public void scrollToPosition(int position) {
if (!mInfinite && (position < 0 || position >= getItemCount())) return;
mPendingScrollPosition = position;
mOffset = mShouldReverseLayout ? position * -mInterval : position * mInterval;
requestLayout();
}
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
if (state.getItemCount() == 0) {
removeAndRecycleAllViews(recycler);
mOffset = 0;
return;
}
ensureLayoutState();
resolveShouldLayoutReverse();
//make sure properties are correct while measure more than once
View scrap = recycler.getViewForPosition(0);
measureChildWithMargins(scrap, 0, 0);
mDecoratedMeasurement = mOrientationHelper.getDecoratedMeasurement(scrap);
mDecoratedMeasurementInOther = mOrientationHelper.getDecoratedMeasurementInOther(scrap);
mSpaceMain = (mOrientationHelper.getTotalSpace() - mDecoratedMeasurement) / 2;
if (mDistanceToBottom == INVALID_SIZE) {
mSpaceInOther = (getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2;
} else {
mSpaceInOther = getTotalSpaceInOther() - mDecoratedMeasurementInOther - mDistanceToBottom;
}
mInterval = setInterval();
setUp();
mLeftItems = (int) Math.abs(minRemoveOffset() / mInterval) + 1;
mRightItems = (int) Math.abs(maxRemoveOffset() / mInterval) + 1;
if (mPendingSavedState != null) {
mShouldReverseLayout = mPendingSavedState.isReverseLayout;
mPendingScrollPosition = mPendingSavedState.position;
mOffset = mPendingSavedState.offset;
}
if (mPendingScrollPosition != NO_POSITION) {
mOffset = mShouldReverseLayout ?
mPendingScrollPosition * -mInterval : mPendingScrollPosition * mInterval;
}
detachAndScrapAttachedViews(recycler);
layoutItems(recycler);
}
public int getTotalSpaceInOther() {
if (mOrientation == HORIZONTAL) {
return getHeight() - getPaddingTop()
- getPaddingBottom();
} else {
return getWidth() - getPaddingLeft()
- getPaddingRight();
}
}
@Override
public void onLayoutCompleted(RecyclerView.State state) {
super.onLayoutCompleted(state);
mPendingSavedState = null;
mPendingScrollPosition = NO_POSITION;
}
@Override
public boolean onAddFocusables(RecyclerView recyclerView, ArrayList<View> views, int direction, int focusableMode) {
final int currentPosition = getCurrentPosition();
final View currentView = findViewByPosition(currentPosition);
if (currentView == null) return true;
if (recyclerView.hasFocus()) {
final int movement = getMovement(direction);
if (movement != DIRECTION_NO_WHERE) {
final int targetPosition = movement == DIRECTION_BACKWARD ?
currentPosition - 1 : currentPosition + 1;
recyclerView.smoothScrollToPosition(targetPosition);
}
} else {
currentView.addFocusables(views, direction, focusableMode);
}
return true;
}
@Override
public View onFocusSearchFailed(View focused, int focusDirection, RecyclerView.Recycler recycler, RecyclerView.State state) {
return null;
}
private int getMovement(int direction) {
if (mOrientation == VERTICAL) {
if (direction == View.FOCUS_UP) {
return mShouldReverseLayout ? DIRECTION_FORWARD : DIRECTION_BACKWARD;
} else if (direction == View.FOCUS_DOWN) {
return mShouldReverseLayout ? DIRECTION_BACKWARD : DIRECTION_FORWARD;
} else {
return DIRECTION_NO_WHERE;
}
} else {
if (direction == View.FOCUS_LEFT) {
return mShouldReverseLayout ? DIRECTION_FORWARD : DIRECTION_BACKWARD;
} else if (direction == View.FOCUS_RIGHT) {
return mShouldReverseLayout ? DIRECTION_BACKWARD : DIRECTION_FORWARD;
} else {
return DIRECTION_NO_WHERE;
}
}
}
void ensureLayoutState() {
if (mOrientationHelper == null) {
mOrientationHelper = OrientationHelper.createOrientationHelper(this, mOrientation);
}
}
/**
* You can set up your own properties here or change the exist properties like mSpaceMain and mSpaceInOther
*/
protected void setUp() {
}
private float getProperty(int position) {
return mShouldReverseLayout ? position * -mInterval : position * mInterval;
}
@Override
public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
removeAllViews();
mOffset = 0;
}
@Override
public int computeHorizontalScrollOffset(RecyclerView.State state) {
return computeScrollOffset();
}
@Override
public int computeVerticalScrollOffset(RecyclerView.State state) {
return computeScrollOffset();
}
@Override
public int computeHorizontalScrollExtent(RecyclerView.State state) {
return computeScrollExtent();
}
@Override
public int computeVerticalScrollExtent(RecyclerView.State state) {
return computeScrollExtent();
}
@Override
public int computeHorizontalScrollRange(RecyclerView.State state) {
return computeScrollRange();
}
@Override
public int computeVerticalScrollRange(RecyclerView.State state) {
return computeScrollRange();
}
private int computeScrollOffset() {
if (getChildCount() == 0) {
return 0;
}
if (!mSmoothScrollbarEnabled) {
return !mShouldReverseLayout ?
getCurrentPosition() : getItemCount() - getCurrentPosition() - 1;
}
final float realOffset = getOffsetOfRightAdapterPosition();
return !mShouldReverseLayout ? (int) realOffset : (int) ((getItemCount() - 1) * mInterval + realOffset);
}
private int computeScrollExtent() {
if (getChildCount() == 0) {
return 0;
}
if (!mSmoothScrollbarEnabled) {
return 1;
}
return (int) mInterval;
}
private int computeScrollRange() {
if (getChildCount() == 0) {
return 0;
}
if (!mSmoothScrollbarEnabled) {
return getItemCount();
}
return (int) (getItemCount() * mInterval);
}
@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mOrientation == VERTICAL) {
return 0;
}
return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (mOrientation == HORIZONTAL) {
return 0;
}
return scrollBy(dy, recycler, state);
}
private int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
if (getChildCount() == 0 || dy == 0) {
return 0;
}
ensureLayoutState();
int willScroll = dy;
float realDx = dy / getDistanceRatio();
if (Math.abs(realDx) < 0.00000001f) {
return 0;
}
float targetOffset = mOffset + realDx;
//handle the boundary
if (!mInfinite && targetOffset < getMinOffset()) {
willScroll -= (targetOffset - getMinOffset()) * getDistanceRatio();
} else if (!mInfinite && targetOffset > getMaxOffset()) {
willScroll = (int) ((getMaxOffset() - mOffset) * getDistanceRatio());
}
realDx = willScroll / getDistanceRatio();
mOffset += realDx;
//handle recycle
layoutItems(recycler);
return willScroll;
}
private void layoutItems(RecyclerView.Recycler recycler) {
detachAndScrapAttachedViews(recycler);
positionCache.clear();
final int itemCount = getItemCount();
if (itemCount == 0) return;
// make sure that current position start from 0 to 1
final int currentPos = mShouldReverseLayout ?
-getCurrentPositionOffset() : getCurrentPositionOffset();
int start = currentPos - mLeftItems;
int end = currentPos + mRightItems;
// handle max visible count
if (useMaxVisibleCount()) {
boolean isEven = mMaxVisibleItemCount % 2 == 0;
if (isEven) {
int offset = mMaxVisibleItemCount / 2;
start = currentPos - offset + 1;
end = currentPos + offset + 1;
} else {
int offset = (mMaxVisibleItemCount - 1) / 2;
start = currentPos - offset;
end = currentPos + offset + 1;
}
}
if (!mInfinite) {
if (start < 0) {
start = 0;
if (useMaxVisibleCount()) end = mMaxVisibleItemCount;
}
if (end > itemCount) end = itemCount;
}
float lastOrderWeight = Float.MIN_VALUE;
for (int i = start; i < end; i++) {
if (useMaxVisibleCount() || !removeCondition(getProperty(i) - mOffset)) {
// start and end base on current position,
// so we need to calculate the mAdapter position
int adapterPosition = i;
if (i >= itemCount) {
adapterPosition %= itemCount;
} else if (i < 0) {
int delta = (-adapterPosition) % itemCount;
if (delta == 0) delta = itemCount;
adapterPosition = itemCount - delta;
}
final View scrap = recycler.getViewForPosition(adapterPosition);
measureChildWithMargins(scrap, 0, 0);
resetViewProperty(scrap);
// we need i to calculate the real offset of current view
final float targetOffset = getProperty(i) - mOffset;
layoutScrap(scrap, targetOffset);
final float orderWeight = mEnableBringCenterToFront ?
setViewElevation(scrap, targetOffset) : adapterPosition;
if (orderWeight > lastOrderWeight) {
addView(scrap);
} else {
addView(scrap, 0);
}
if (i == currentPos) currentFocusView = scrap;
lastOrderWeight = orderWeight;
positionCache.put(i, scrap);
}
}
currentFocusView.requestFocus();
}
private boolean useMaxVisibleCount() {
return mMaxVisibleItemCount != DETERMINE_BY_MAX_AND_MIN;
}
private boolean removeCondition(float targetOffset) {
return targetOffset > maxRemoveOffset() || targetOffset < minRemoveOffset();
}
private void resetViewProperty(View v) {
v.setRotation(0);
v.setRotationY(0);
v.setRotationX(0);
v.setScaleX(1f);
v.setScaleY(1f);
v.setAlpha(1f);
}
/* package */ float getMaxOffset() {
return !mShouldReverseLayout ? (getItemCount() - 1) * mInterval : 0;
}
/* package */ float getMinOffset() {
return !mShouldReverseLayout ? 0 : -(getItemCount() - 1) * mInterval;
}
private void layoutScrap(View scrap, float targetOffset) {
final int left = calItemLeft(scrap, targetOffset);
final int top = calItemTop(scrap, targetOffset);
if (mOrientation == VERTICAL) {
layoutDecorated(scrap, mSpaceInOther + left, mSpaceMain + top,
mSpaceInOther + left + mDecoratedMeasurementInOther, mSpaceMain + top + mDecoratedMeasurement);
} else {
layoutDecorated(scrap, mSpaceMain + left, mSpaceInOther + top,
mSpaceMain + left + mDecoratedMeasurement, mSpaceInOther + top + mDecoratedMeasurementInOther);
}
setItemViewProperty(scrap, targetOffset);
}
protected int calItemLeft(View itemView, float targetOffset) {
return mOrientation == VERTICAL ? 0 : (int) targetOffset;
}
protected int calItemTop(View itemView, float targetOffset) {
return mOrientation == VERTICAL ? (int) targetOffset : 0;
}
/**
* when the target offset reach this,
* the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)}
*/
protected float maxRemoveOffset() {
return mOrientationHelper.getTotalSpace() - mSpaceMain;
}
/**
* when the target offset reach this,
* the view will be removed and recycled in {@link #layoutItems(RecyclerView.Recycler)}
*/
protected float minRemoveOffset() {
return -mDecoratedMeasurement - mOrientationHelper.getStartAfterPadding() - mSpaceMain;
}
public int getCurrentPosition() {
if (getItemCount() == 0) return 0;
int position = getCurrentPositionOffset();
if (!mInfinite) return Math.abs(position);
position = !mShouldReverseLayout ?
//take care of position = getItemCount()
(position >= 0 ?
position % getItemCount() :
getItemCount() + position % getItemCount()) :
(position > 0 ?
getItemCount() - position % getItemCount() :
-position % getItemCount());
return position == getItemCount() ? 0 : position;
}
@Override
public View findViewByPosition(int position) {
final int itemCount = getItemCount();
if (itemCount == 0) return null;
for (int i = 0; i < positionCache.size(); i++) {
final int key = positionCache.keyAt(i);
if (key >= 0) {
if (position == key % itemCount) return positionCache.valueAt(i);
} else {
int delta = key % itemCount;
if (delta == 0) delta = -itemCount;
if (itemCount + delta == position) return positionCache.valueAt(i);
}
}
return null;
}
private int getCurrentPositionOffset() {
return Math.round(mOffset / mInterval);
}
/**
* Sometimes we need to get the right offset of matching mAdapter position
* cause when {@link #mInfinite} is set true, there will be no limitation of {@link #mOffset}
*/
private float getOffsetOfRightAdapterPosition() {
if (mShouldReverseLayout)
return mInfinite ?
(mOffset <= 0 ?
(mOffset % (mInterval * getItemCount())) :
(getItemCount() * -mInterval + mOffset % (mInterval * getItemCount()))) :
mOffset;
else
return mInfinite ?
(mOffset >= 0 ?
(mOffset % (mInterval * getItemCount())) :
(getItemCount() * mInterval + mOffset % (mInterval * getItemCount()))) :
mOffset;
}
/**
* @return the dy between center and current position
*/
public int getOffsetToCenter() {
if (mInfinite)
return (int) ((getCurrentPositionOffset() * mInterval - mOffset) * getDistanceRatio());
return (int) ((getCurrentPosition() *
(!mShouldReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio());
}
public int getOffsetToPosition(int position) {
if (mInfinite)
return (int) (((getCurrentPositionOffset() +
(!mShouldReverseLayout ? position - getCurrentPosition() : getCurrentPosition() - position)) *
mInterval - mOffset) * getDistanceRatio());
return (int) ((position *
(!mShouldReverseLayout ? mInterval : -mInterval) - mOffset) * getDistanceRatio());
}
public void setOnPageChangeListener(OnPageChangeListener onPageChangeListener) {
this.onPageChangeListener = onPageChangeListener;
}
public void setInfinite(boolean enable) {
assertNotInLayoutOrScroll(null);
if (enable == mInfinite) {
return;
}
mInfinite = enable;
requestLayout();
}
public boolean getInfinite() {
return mInfinite;
}
public int getDistanceToBottom() {
return mDistanceToBottom == INVALID_SIZE ?
(getTotalSpaceInOther() - mDecoratedMeasurementInOther) / 2 : mDistanceToBottom;
}
public void setDistanceToBottom(int mDistanceToBottom) {
assertNotInLayoutOrScroll(null);
if (this.mDistanceToBottom == mDistanceToBottom) return;
this.mDistanceToBottom = mDistanceToBottom;
removeAllViews();
}
/**
* When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed
* based on the number of visible pixels in the visible items. This however assumes that all
* list items have similar or equal widths or heights (depending on list orientation).
* If you use a list in which items have different dimensions, the scrollbar will change
* appearance as the user scrolls through the list. To avoid this issue, you need to disable
* this property.
* <p>
* When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based
* solely on the number of items in the mAdapter and the position of the visible items inside
* the mAdapter. This provides a stable scrollbar as the user navigates through a list of items
* with varying widths / heights.
*
* @param enabled Whether or not to enable smooth scrollbar.
* @see #setSmoothScrollbarEnabled(boolean)
*/
public void setSmoothScrollbarEnabled(boolean enabled) {
mSmoothScrollbarEnabled = enabled;
}
public void setEnableBringCenterToFront(boolean bringCenterToTop) {
assertNotInLayoutOrScroll(null);
if (mEnableBringCenterToFront == bringCenterToTop) {
return;
}
this.mEnableBringCenterToFront = bringCenterToTop;
requestLayout();
}
public boolean getEnableBringCenterToFront() {
return mEnableBringCenterToFront;
}
/**
* Returns the current state of the smooth scrollbar feature. It is enabled by default.
*
* @return True if smooth scrollbar is enabled, false otherwise.
* @see #setSmoothScrollbarEnabled(boolean)
*/
public boolean getSmoothScrollbarEnabled() {
return mSmoothScrollbarEnabled;
}
private static class SavedState implements Parcelable {
int position;
float offset;
boolean isReverseLayout;
SavedState() {
}
SavedState(Parcel in) {
position = in.readInt();
offset = in.readFloat();
isReverseLayout = in.readInt() == 1;
}
public SavedState(SavedState other) {
position = other.position;
offset = other.offset;
isReverseLayout = other.isReverseLayout;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(position);
dest.writeFloat(offset);
dest.writeInt(isReverseLayout ? 1 : 0);
}
public static final Creator<SavedState> CREATOR
= new Creator<SavedState>() {
@Override
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
@Override
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}
public interface OnPageChangeListener {
void onPageSelected(int position);
void onPageScrollStateChanged(int state);
}
}
CenterSnapHelper
package com.xuexiang.xui.widget.banner.recycler.layout;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;
/**
* Class intended to support snapping for a {@link RecyclerView}
* which use {@link BannerLayoutManager} as its {@link LayoutManager}.
* <p>
* The implementation will snap the center of the target child view to the center of
* the attached {@link RecyclerView}.
*/
public class CenterSnapHelper extends RecyclerView.OnFlingListener {
RecyclerView mRecyclerView;
Scroller mGravityScroller;
/**
* when the dataSet is extremely large
* {@link #snapToCenterView(BannerLayoutManager, BannerLayoutManager.OnPageChangeListener)}
* may keep calling itself because the accuracy of float
*/
private boolean snapToCenter = false;
// Handles the snap on scroll case.
private final RecyclerView.OnScrollListener mScrollListener =
new RecyclerView.OnScrollListener() {
boolean mScrolled = false;
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
final BannerLayoutManager layoutManager =
(BannerLayoutManager) recyclerView.getLayoutManager();
final BannerLayoutManager.OnPageChangeListener onPageChangeListener =
layoutManager.onPageChangeListener;
if (onPageChangeListener != null) {
onPageChangeListener.onPageScrollStateChanged(newState);
}
if (newState == RecyclerView.SCROLL_STATE_IDLE && mScrolled) {
mScrolled = false;
if (!snapToCenter) {
snapToCenter = true;
snapToCenterView(layoutManager, onPageChangeListener);
} else {
snapToCenter = false;
}
}
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if (dx != 0 || dy != 0) {
mScrolled = true;
}
}
};
@Override
public boolean onFling(int velocityX, int velocityY) {
BannerLayoutManager layoutManager = (BannerLayoutManager) mRecyclerView.getLayoutManager();
if (layoutManager == null) {
return false;
}
RecyclerView.Adapter adapter = mRecyclerView.getAdapter();
if (adapter == null) {
return false;
}
if (!layoutManager.getInfinite() &&
(layoutManager.mOffset == layoutManager.getMaxOffset()
|| layoutManager.mOffset == layoutManager.getMinOffset())) {
return false;
}
final int minFlingVelocity = mRecyclerView.getMinFlingVelocity();
mGravityScroller.fling(0, 0, velocityX, velocityY,
Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE);
if (layoutManager.mOrientation == BannerLayoutManager.VERTICAL
&& Math.abs(velocityY) > minFlingVelocity) {
final int currentPosition = layoutManager.getCurrentPosition();
final int offsetPosition = (int) (mGravityScroller.getFinalY() /
layoutManager.mInterval / layoutManager.getDistanceRatio());
mRecyclerView.smoothScrollToPosition(layoutManager.getReverseLayout() ?
currentPosition - offsetPosition : currentPosition + offsetPosition);
return true;
} else if (layoutManager.mOrientation == BannerLayoutManager.HORIZONTAL
&& Math.abs(velocityX) > minFlingVelocity) {
final int currentPosition = layoutManager.getCurrentPosition();
final int offsetPosition = (int) (mGravityScroller.getFinalX() /
layoutManager.mInterval / layoutManager.getDistanceRatio());
mRecyclerView.smoothScrollToPosition(layoutManager.getReverseLayout() ?
currentPosition - offsetPosition : currentPosition + offsetPosition);
return true;
}
return true;
}
/**
* Please attach after {{@link LayoutManager} is setting}
* Attaches the {@link CenterSnapHelper} to the provided RecyclerView, by calling
* {@link RecyclerView#setOnFlingListener(RecyclerView.OnFlingListener)}.
* You can call this method with {@code null} to detach it from the current RecyclerView.
*
* @param recyclerView The RecyclerView instance to which you want to add this helper or
* {@code null} if you want to remove CenterSnapHelper from the current
* RecyclerView.
* @throws IllegalArgumentException if there is already a {@link RecyclerView.OnFlingListener}
* attached to the provided {@link RecyclerView}.
*/
public void attachToRecyclerView(@Nullable RecyclerView recyclerView)
throws IllegalStateException {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (mRecyclerView != null) {
final LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (!(layoutManager instanceof BannerLayoutManager)) return;
setupCallbacks();
mGravityScroller = new Scroller(mRecyclerView.getContext(),
new DecelerateInterpolator());
snapToCenterView((BannerLayoutManager) layoutManager,
((BannerLayoutManager) layoutManager).onPageChangeListener);
}
}
void snapToCenterView(BannerLayoutManager layoutManager,
BannerLayoutManager.OnPageChangeListener listener) {
final int delta = layoutManager.getOffsetToCenter();
if (delta != 0) {
if (layoutManager.getOrientation()
== BannerLayoutManager.VERTICAL) {
mRecyclerView.smoothScrollBy(0, delta);
} else {
mRecyclerView.smoothScrollBy(delta, 0);
}
} else {
// set it false to make smoothScrollToPosition keep trigger the listener
snapToCenter = false;
}
if (listener != null)
listener.onPageSelected(layoutManager.getCurrentPosition());
}
/**
* Called when an instance of a {@link RecyclerView} is attached.
*/
void setupCallbacks() throws IllegalStateException {
if (mRecyclerView.getOnFlingListener() != null) {
throw new IllegalStateException("An instance of OnFlingListener already set.");
}
mRecyclerView.addOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(this);
}
/**
* Called when the instance of a {@link RecyclerView} is detached.
*/
void destroyCallbacks() {
mRecyclerView.removeOnScrollListener(mScrollListener);
mRecyclerView.setOnFlingListener(null);
}
}
ResUtils
package com.xuexiang.xui.utils;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import androidx.annotation.AnimRes;
import androidx.annotation.ArrayRes;
import androidx.annotation.ColorRes;
import androidx.annotation.DimenRes;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import androidx.annotation.StyleableRes;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.content.ContextCompat;
import com.xuexiang.xui.XUI;
/**
* 获取res中的资源
*
* @author lk
*/
public final class ResUtils {
private ResUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* 获取resources对象
*
* @return
*/
public static Resources getResources() {
return XUI.getContext().getResources();//自行获取Context
}
/**
* 获取字符串
*
* @param resId
* @return
*/
public static String getString(@StringRes int resId) {
return getResources().getString(resId);
}
/**
* 获取资源图片
*
* @param resId
* @return
*/
public static Drawable getDrawable(@DrawableRes int resId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return XUI.getContext().getDrawable(resId);
}
return getResources().getDrawable(resId);
}
/**
* 获取资源图片【和主体有关】
*
* @param resId
* @return
*/
public static Drawable getDrawable(Context context, @DrawableRes int resId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return context.getDrawable(resId);
}
return AppCompatResources.getDrawable(context, resId);
}
/**
* 获取svg资源图片
*
* @param context
* @param resId
* @return
*/
public static Drawable getVectorDrawable(Context context, @DrawableRes int resId) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return context.getDrawable(resId);
}
return AppCompatResources.getDrawable(context, resId);
}
/**
* 获取Drawable属性(兼容VectorDrawable)
*
* @param context
* @param typedArray
* @param index
* @return
*/
public static Drawable getDrawableAttrRes(Context context, TypedArray typedArray, @StyleableRes int index) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
return typedArray.getDrawable(index);
} else {
int resourceId = typedArray.getResourceId(index, -1);
if (resourceId != -1) {
return AppCompatResources.getDrawable(context, resourceId);
}
}
return null;
}
/**
* 获取dimes值,返回的是精确的值
*
* @param resId
* @return
*/
public static float getDimens(@DimenRes int resId) {
return getResources().getDimension(resId);
}
/**
* 获取Color值
*
* @param resId
* @return
*/
public static int getColor(@ColorRes int resId) {
return getResources().getColor(resId);
}
/**
* 获取ColorStateList值
*
* @param resId
* @return
*/
public static ColorStateList getColors(@ColorRes int resId) {
return getResources().getColorStateList(resId);
}
/**
* 获取dimes值,返回的是【去余取整】的值
*
* @param resId
* @return
*/
public static int getDimensionPixelOffset(@DimenRes int resId) {
return getResources().getDimensionPixelOffset(resId);
}
/**
* 获取dimes值,返回的是【4舍5入】的值
*
* @param resId
* @return
*/
public static int getDimensionPixelSize(@DimenRes int resId) {
return getResources().getDimensionPixelSize(resId);
}
/**
* 获取字符串的数组
*
* @param resId
* @return
*/
public static String[] getStringArray(@ArrayRes int resId) {
return getResources().getStringArray(resId);
}
/**
* 获取Drawable的数组
*
* @param context
* @param resId
* @return
*/
public static Drawable[] getDrawableArray(Context context, @ArrayRes int resId) {
TypedArray ta = getResources().obtainTypedArray(resId);
Drawable[] icons = new Drawable[ta.length()];
for (int i = 0; i < ta.length(); i++) {
int id = ta.getResourceId(i, 0);
if (id != 0) {
icons[i] = ContextCompat.getDrawable(context, id);
}
}
ta.recycle();
return icons;
}
/**
* 获取数字的数组
*
* @param resId
* @return
*/
public static int[] getIntArray(@ArrayRes int resId) {
return getResources().getIntArray(resId);
}
/**
* 获取动画
*
* @param resId
* @return
*/
public static Animation getAnim(@AnimRes int resId) {
return AnimationUtils.loadAnimation(XUI.getContext(), resId);
}
/**
* Check if layout direction is RTL
*
* @return {@code true} if the layout direction is right-to-left
*/
public static boolean isRtl() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 &&
getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
}
/**
* Darkens a color by a given factor.
*
* @param color the color to darken
* @param factor The factor to darken the color.
* @return darker version of specified color.
*/
public static int darker(int color, float factor) {
return Color.argb(Color.alpha(color), Math.max((int) (Color.red(color) * factor), 0),
Math.max((int) (Color.green(color) * factor), 0),
Math.max((int) (Color.blue(color) * factor), 0));
}
/**
* Lightens a color by a given factor.
*
* @param color The color to lighten
* @param factor The factor to lighten the color. 0 will make the color unchanged. 1 will make the
* color white.
* @return lighter version of the specified color.
*/
public static int lighter(int color, float factor) {
int red = (int) ((Color.red(color) * (1 - factor) / 255 + factor) * 255);
int green = (int) ((Color.green(color) * (1 - factor) / 255 + factor) * 255);
int blue = (int) ((Color.blue(color) * (1 - factor) / 255 + factor) * 255);
return Color.argb(Color.alpha(color), red, green, blue);
}
}
styleable
<declare-styleable name="BannerLayout">
<!--轮播间隔,单位ms,默认是4000ms-->
<attr name="bl_interval" format="integer" />
<!--是否显示轮播索引,默认是true-->
<attr name="bl_showIndicator" format="boolean" />
<!--轮播的方向,默认是horizontal-->
<attr name="bl_orientation" format="enum">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<!--是否是自动轮播,默认是true-->
<attr name="bl_autoPlaying" format="boolean" />
<!--轮播索引选中的效果-->
<attr name="bl_indicatorSelectedSrc" format="reference" />
<!--轮播索引未选中的效果-->
<attr name="bl_indicatorUnselectedSrc" format="reference" />
<!--轮播索引的大小,默认5dp-->
<attr name="bl_indicatorSize" format="dimension" />
<!--轮播索引选中的颜色-->
<attr name="bl_indicatorSelectedColor" format="color" />
<!--轮播索引未选中的颜色-->
<attr name="bl_indicatorUnselectedColor" format="color" />
<!--索引器之间的间隔,默认4dp-->
<attr name="bl_indicatorSpace" format="dimension" />
<!--索引器的左侧边距,默认16dp-->
<attr name="bl_indicatorMarginLeft" format="dimension" />
<!--索引器的右侧边距,默认0dp-->
<attr name="bl_indicatorMarginRight" format="dimension" />
<!--索引器的底部边距,默认11dp-->
<attr name="bl_indicatorMarginBottom" format="dimension" />
<!--索引器的对齐方式,默认left-->
<attr name="bl_indicatorGravity" format="enum">
<enum name="left" value="0" />
<enum name="center" value="1" />
<enum name="right" value="2" />
</attr>
<!--轮播图之间的间距,默认是10dp-->
<attr name="bl_itemSpace" format="dimension" />
<!--图片缩放系数,默认是1.2F-->
<attr name="bl_centerScale" format="float" />
<!--轮播速度,默认是1.0F-->
<attr name="bl_moveSpeed" format="float" />
</declare-styleable>
RecyclerViewBannerAdapter
package com.xuexiang.xuidemo.adapter;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import com.xuexiang.xui.adapter.recyclerview.BaseRecyclerAdapter;
import com.xuexiang.xui.adapter.recyclerview.RecyclerViewHolder;
import com.xuexiang.xui.widget.banner.recycler.BannerLayout;
import com.xuexiang.xui.widget.imageview.ImageLoader;
import com.xuexiang.xui.widget.imageview.strategy.DiskCacheStrategyEnum;
import com.xuexiang.xuidemo.R;
import java.util.Arrays;
import java.util.List;
/**
* @author lk
*/
public class RecyclerViewBannerAdapter extends BaseRecyclerAdapter<String> {
/**
* 默认加载图片
*/
private ColorDrawable mColorDrawable;
/**
* 是否允许进行缓存
*/
private boolean mEnableCache = true;
private BannerLayout.OnBannerItemClickListener mOnBannerItemClickListener;
public RecyclerViewBannerAdapter() {
super();
mColorDrawable = new ColorDrawable(Color.parseColor("#555555"));
}
public RecyclerViewBannerAdapter(List<String> list) {
super(list);
mColorDrawable = new ColorDrawable(Color.parseColor("#555555"));
}
public RecyclerViewBannerAdapter(String[] list) {
super(Arrays.asList(list));
mColorDrawable = new ColorDrawable(Color.parseColor("#555555"));
}
/**
* 适配的布局
*
* @param viewType
* @return
*/
@Override
public int getItemLayoutId(int viewType) {
return R.layout.adapter_recycler_view_banner_image_item;
}
/**
* 绑定数据
*
* @param holder
* @param position
* @param imgUrl
*/
@Override
public void bindData(@NonNull RecyclerViewHolder holder, final int position, String imgUrl) {
ImageView imageView = holder.findViewById(R.id.iv_item);
if (!TextUtils.isEmpty(imgUrl)) {
ImageLoader.get().loadImage(imageView, imgUrl, mColorDrawable,
mEnableCache ? DiskCacheStrategyEnum.RESOURCE : DiskCacheStrategyEnum.NONE);
} else {
imageView.setImageDrawable(mColorDrawable);
}
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnBannerItemClickListener != null) {
mOnBannerItemClickListener.onItemClick(position);
}
}
});
}
/**
* 设置是否允许缓存
*
* @param enableCache
* @return
*/
public RecyclerViewBannerAdapter enableCache(boolean enableCache) {
mEnableCache = enableCache;
return this;
}
/**
* 获取是否允许缓存
*
* @return
*/
public boolean getEnableCache() {
return mEnableCache;
}
public ColorDrawable getColorDrawable() {
return mColorDrawable;
}
public RecyclerViewBannerAdapter setColorDrawable(ColorDrawable colorDrawable) {
mColorDrawable = colorDrawable;
return this;
}
public RecyclerViewBannerAdapter setOnBannerItemClickListener(BannerLayout.OnBannerItemClickListener onBannerItemClickListener) {
mOnBannerItemClickListener = onBannerItemClickListener;
return this;
}
}