实现原理:
1到second last为实际数据,0跟last为banner自动填充的item,其中0中数据跟second last项数据相同,last跟1项数据相同。
当banner滑动(或者手动滑动)到0时,banner静默滚动到second last处(因为0跟second last图片相同所以用户无感知);当banner滑动到last时,banner静默滚动到1处。
代码:
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.drawable.GradientDrawable;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
//TODO 根据自己引用的RecyclerView修改引用
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSnapHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import com.huawei.glass.R;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/**
* Created by weixing on 2017/6/30.
*/
public class RecyclerBanner extends FrameLayout {
/**导航圆点类型*/
public static final short NAV_DOT = 0;
/**导航条类型*/
public static final short NAV_BOTTOM_BAR = 1;
/**导航数字类型*/
public static final short NAV_NUMBER = 2;
private RecyclerView mRecyclerView;
/**导航条及点的父控件*/
private LinearLayout mDotContainer;
/**导航dot及bar的drawable*/
private GradientDrawable defaultDrawable, selectedDrawable;
/**导航类型*/
private int mNavType = 0;
/**导航条宽度*/
private int mNavBarWidth;
/**导航文本控件*/
private TextView mNavNumber;
/**导航控件底部边距*/
private int navBottomMargin = 30;
/**默认导航颜色及选中导航颜色*/
private int defaultColor = 0x75ffffff, selectColor = 0xffFF6822;
private RecyclerAdapter mAdapter;
private OnPagerClickListener mPageClickListener;
private List<BannerEntity> mData = new ArrayList<>();
/**圆点大小*/
private int mDotSize;
/**手指触屏位置*/
private int startX, startY;
/**当前滚动位置*/
private int currentIndex;
/**是否滚动*/
private boolean isPlaying;
public interface OnPagerClickListener {
void onClick(BannerEntity entity);
}
public static interface BannerEntity {
String getUrl();
}
@SuppressLint("HandlerLeak")
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
currentIndex = currentIndex >= mData.size() - 1 ? 1 : ++currentIndex;
if (currentIndex >= mData.size()) {
currentIndex = currentIndex % mData.size();
}
mRecyclerView.smoothScrollToPosition(currentIndex);
sendEmptyMessageDelayed(0, 3000);
}
};
public RecyclerBanner(Context context) {
this(context, null);
}
public RecyclerBanner(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RecyclerBanner(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// defaultDrawable = new GradientDrawable();
// defaultDrawable.setSize(mDotSize, mDotSize);
// defaultDrawable.setCornerRadius(mDotSize);
// defaultDrawable.setColor(0xffffffff);
// selectedDrawable = new GradientDrawable();
// selectedDrawable.setSize(mDotSize, mDotSize);
// selectedDrawable.setCornerRadius(mDotSize);
// selectedDrawable.setColor(0xff0094ff);
LayoutParams vpLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
mDotContainer = new LinearLayout(context);
mDotContainer.setOrientation(LinearLayout.HORIZONTAL);
LayoutParams linearLayoutParams = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mDotContainer.setGravity(Gravity.CENTER);
linearLayoutParams.setMargins(0, 0, 0, navBottomMargin);
mDotContainer.setPadding(mDotSize * 2, mDotSize * 2, mDotSize * 2, mDotSize * 2);
linearLayoutParams.gravity = Gravity.BOTTOM;
mRecyclerView = new RecyclerView(context);
new PagerSnapHelper().attachToRecyclerView(mRecyclerView);
mRecyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
mAdapter = new RecyclerAdapter();
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NotNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
if (touched) {
int first = ((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition();
int last = ((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition();
int current;
if (first == 0) {
current = (int) Math.round((first + last) / 2.0 - 0.1);
} else {
current = (int) Math.round((first + last) / 2.0);
}
if (current == (mData.size() - 1)) {
current = 1;
mRecyclerView.scrollToPosition(1);
} else if (current == 0) {
current = mData.size() - 2;
mRecyclerView.scrollToPosition(mData.size() - 2);
}
if (currentIndex != current) {
currentIndex = current;
changePoint();
}
touched = false;
return;
}
if (currentIndex == mData.size() - 1) {
mRecyclerView.scrollToPosition(1);
currentIndex = 1;
}
changePoint();
}
}
});
addView(mRecyclerView, vpLayoutParams);
addView(mDotContainer, linearLayoutParams);
mNavBarWidth = (getResources().getDisplayMetrics().widthPixels - 160) / (mData.size() == 0 ? 5 : mData.size());
mDotSize = (int) (6 * context.getResources().getDisplayMetrics().density + 0.5f);
setNavColor(defaultColor, selectColor);
initNavView();
/*
TODO 为方便使用没有在属性xml文件中定义属性,如果要使用则直接调用类方法设置比例
//初始化比例
TypedArray a = getContext().obtainStyledAttributes(attrs,
R.styleable.RatioImageView);
mIsWidthFitDrawableSizeRatio = a.getBoolean(R.styleable.RatioImageView_is_width_fix_drawable_size_ratio,
mIsWidthFitDrawableSizeRatio);
mIsHeightFitDrawableSizeRatio = a.getBoolean(R.styleable.RatioImageView_is_height_fix_drawable_size_ratio,
mIsHeightFitDrawableSizeRatio);
mHeightRatio = a.getFloat(
R.styleable.RatioImageView_height_to_width_ratio, mHeightRatio);
mWidthRatio = a.getFloat(
R.styleable.RatioImageView_width_to_height_ratio, mWidthRatio);
a.recycle();*/
}
public void setOnPagerClickListener(OnPagerClickListener onPagerClickListener) {
this.mPageClickListener = onPagerClickListener;
}
/**设置是否应该滚动*/
public synchronized void setPlaying(boolean playing) {
if (!isPlaying && playing && mAdapter != null && mAdapter.getItemCount() > 2) {
mHandler.removeMessages(0);
mHandler.sendEmptyMessageDelayed(0, 3000);
isPlaying = true;
} else if (isPlaying && !playing) {
mHandler.removeMessages(0);
isPlaying = false;
}
}
/**
* 设置data需在setColor及setNavType之后
* @param datas 数据列表
* @return 数据数量
*/
public int setDatas(List<? extends BannerEntity> datas) {
setPlaying(false);
mDotContainer.removeAllViews();
if (mData != null) {
this.mData.clear();
} else {
this.mData = new ArrayList<>();
}
this.mData.addAll(datas);
if (this.mData.size() > 1) {
currentIndex = 1;//设置无限循环播放
mAdapter.notifyDataSetChanged();
mRecyclerView.scrollToPosition(currentIndex);
// for (int i = 0; i < this.mData.size(); i++) {
// ImageView img = new ImageView(getContext());
// LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// lp.leftMargin = mDotSize / 2;
// lp.rightMargin = mDotSize / 2;
// img.setImageDrawable(i == 0 ? selectedDrawable : defaultDrawable);
// mDotContainer.addView(img, lp);
// }
if (mNavType != NAV_NUMBER) {
for (int i = 0; i < this.mData.size(); i++) {
ImageView img = new ImageView(getContext());
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.leftMargin = mDotSize / 2;
lp.rightMargin = mDotSize / 2;
img.setImageDrawable(i == 0 ? selectedDrawable : defaultDrawable);
mDotContainer.addView(img, lp);
}
} else {
mNavNumber.setText(String.format(Locale.CHINA, "1/%d", datas.size()));
}
setPlaying(true);
mData.add(0, mData.get(mData.size() - 1));
mData.add(mData.get(1));
} else {
currentIndex = 0;
mAdapter.notifyDataSetChanged();
}
return this.mData.size();
}
private void initNavView() {
initNavView(mNavType);
}
/**初始化导航类型*/
private void initNavView(int type){
if (type == NAV_DOT) {
if (mDotContainer != null) {
mDotContainer.setVisibility(VISIBLE);
}
if (mNavNumber != null && mNavNumber.getVisibility() == VISIBLE) {
mNavNumber.setVisibility(GONE);
}
defaultDrawable = new GradientDrawable();
defaultDrawable.setSize(mDotSize, mDotSize);
defaultDrawable.setCornerRadius(mDotSize);
defaultDrawable.setColor(defaultColor);
selectedDrawable = new GradientDrawable();
selectedDrawable.setSize(mDotSize, mDotSize);
selectedDrawable.setCornerRadius(mDotSize);
selectedDrawable.setColor(selectColor);
} else if (type == NAV_BOTTOM_BAR) {
if (mDotContainer != null) {
mDotContainer.setVisibility(VISIBLE);
}
if (mNavNumber != null && mNavNumber.getVisibility() == VISIBLE) {
mNavNumber.setVisibility(GONE);
}
defaultDrawable = new GradientDrawable();
defaultDrawable.setSize(mNavBarWidth, mDotSize / 3);
defaultDrawable.setCornerRadius(0);
defaultDrawable.setColor(defaultColor);
selectedDrawable = new GradientDrawable();
selectedDrawable.setSize(mNavBarWidth, mDotSize / 3);
selectedDrawable.setCornerRadius(0);
selectedDrawable.setColor(selectColor);
} else if (type == NAV_NUMBER) {
if (mDotContainer != null) {
mDotContainer.setVisibility(GONE);
mDotContainer.removeAllViews();
}
if (mNavNumber != null) {
mNavNumber.setVisibility(VISIBLE);
return;
}
mNavNumber = new TextView(getContext());
mNavNumber.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
//设置背景
int roundRadius = 20; // 8dp 圆角半径
int fillColor = defaultColor;//内部填充颜色
GradientDrawable gd = new GradientDrawable();//创建drawable(动态创建shape)
gd.setColor(fillColor);
gd.setCornerRadius(roundRadius);
mNavNumber.setBackground(gd);
//设置边框
// int strokeColor = Color.parseColor("#2E3135");//边框颜色
// int strokeWidth = 5; // 3dp 边框宽度
// gd.setStroke(strokeWidth, strokeColor);
mNavNumber.setGravity(Gravity.CENTER);
mNavNumber.setPadding(15, 6, 15, 6);
mNavNumber.setText("0/0");
mNavNumber.setTextColor(selectColor);
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.END | Gravity.BOTTOM;
params.bottomMargin = navBottomMargin;
params.rightMargin = navBottomMargin;
addView(mNavNumber, params);
}
}
private boolean touched = false;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
touched = true;
startX = (int) ev.getX();
startY = (int) ev.getY();
getParent().requestDisallowInterceptTouchEvent(true);
setPlaying(false);
break;
case MotionEvent.ACTION_MOVE:
int moveX = (int) ev.getX();
int moveY = (int) ev.getY();
int disX = moveX - startX;
int disY = moveY - startY;
getParent().requestDisallowInterceptTouchEvent(2 * Math.abs(disX) > Math.abs(disY));
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) {
if (visibility == View.GONE) {
//页面不可见时停止轮播
setPlaying(false);
} else if (visibility == View.VISIBLE) {
// 恢复可见时开始轮播
setPlaying(true);
}
super.onWindowVisibilityChanged(visibility);
}
/**
* height width ratio,宽高一个固定另一个不固定时才有效果
*/
private float ratio = 0.5f;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取宽度的模式和尺寸
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
//获取高度的模式和尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//宽确定,高不确定
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY && ratio != 0) {
heightSize = (int) (widthSize * ratio + 0.5f);//根据宽度和比例计算高度
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
} else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY & ratio != 0) {
widthSize = (int) (heightSize / ratio + 0.5f);
widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
}
//必须调用下面的两个方法之一完成onMeasure方法的重写,否则会报错
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
public void setRatio(float ratio) {
this.ratio = ratio;
}
/**
* 设置导航点、条、数字颜色,数字颜色不用加selectColor
* @param defaultColor unSelectColor
* @param selectColor selectedColor
*/
public void setNavColor(int defaultColor, int selectColor){
this.defaultColor = defaultColor;
this.selectColor = selectColor;
if (mNavType == NAV_DOT || mNavType == NAV_BOTTOM_BAR) {
if (defaultDrawable != null) {
defaultDrawable.setColor(defaultColor);
}
if (selectedDrawable != null) {
selectedDrawable.setColor(selectColor);
}
} else {
if (mNavNumber != null) {
mNavNumber.setTextColor(selectColor);
}
}
}
/**
* set nav type
* @param navType type of nav(dot bar or number)
*/
public void setNavType(int navType) {
if (navType != mNavType) {
initNavView(navType);
this.mNavType = navType;
}
}
public void onDestroy() {
setPlaying(false);
mHandler.removeMessages(0);
mRecyclerView.setAdapter(null);
mRecyclerView = null;
mData.clear();
mData = null;
mAdapter = null;
}
// 内置适配器
private class RecyclerAdapter extends RecyclerView.Adapter implements OnClickListener {
@NotNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
SimpleDraweeView img = new SimpleDraweeView(parent.getContext());
RecyclerView.LayoutParams l = new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
img.setScaleType(ImageView.ScaleType.CENTER_CROP);
img.setLayoutParams(l);
img.setId(R.id.icon);
img.setOnClickListener(this);
return new RecyclerView.ViewHolder(img) {
};
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {//可自己修改使用哪种方式加载图片
// ImageView img = (ImageView) holder.itemView.findViewById(R.id.icon);
((SimpleDraweeView) holder.itemView).setImageURI(mData.get(position % mData.size()).getUrl());
// Glide.with(getContext()).load(mData.get(position % mData.size()).getUrl()).placeholder(R.mipmap.ic_launcher).error(R.drawable.arrow_left).into(img);
}
@Override
public int getItemCount() {
return mData == null ? 0 : mData.size();
}
@Override
public void onClick(View v) {
if (mPageClickListener != null) {
mPageClickListener.onClick(mData.get(currentIndex));// TODO: 2017/7/6 返回位置或者广告链接
}
}
}
private class PagerSnapHelper extends LinearSnapHelper {
@Override
public int findTargetSnapPosition(RecyclerView.LayoutManager layoutManager, int velocityX, int velocityY) {
int targetPos = super.findTargetSnapPosition(layoutManager, velocityX, velocityY);
final View currentView = findSnapView(layoutManager);
if (targetPos != RecyclerView.NO_POSITION && currentView != null) {
int currentPostion = layoutManager.getPosition(currentView);
int first = ((LinearLayoutManager) layoutManager).findFirstVisibleItemPosition();
int last = ((LinearLayoutManager) layoutManager).findLastVisibleItemPosition();
currentPostion = targetPos < currentPostion ? last : (targetPos > currentPostion ? first : currentPostion);
targetPos = targetPos < currentPostion ? currentPostion - 1 : (targetPos > currentPostion ? currentPostion + 1 : currentPostion);
}
return targetPos;
}
}
private void changePoint() {
if (mNavType == NAV_DOT || mNavType == NAV_BOTTOM_BAR) {
if (mDotContainer != null && mDotContainer.getChildCount() > 0) {
int childCount = mDotContainer.getChildCount();
for (int i = 0; i < childCount; i++) {
((ImageView) mDotContainer.getChildAt(i)).setImageDrawable(defaultDrawable);
}
if (currentIndex == 0) {
((ImageView) mDotContainer.getChildAt(childCount - 1)).setImageDrawable(selectedDrawable);
} else if (currentIndex == mData.size() - 1) {
((ImageView) mDotContainer.getChildAt(0)).setImageDrawable(selectedDrawable);
} else {
((ImageView) mDotContainer.getChildAt(currentIndex - 1)).setImageDrawable(selectedDrawable);
}
}
} else {
mNavNumber.setText(String.format(Locale.CHINA, "%d/%d", currentIndex, mData.size() - 2));
}
}
}
注:项目重要引入fresco的依赖用于加载图片:
implementation ‘com.facebook.fresco:fresco:1.10.0’//展示相册列表
;当然也可以修改onBindViewHolder中加载图片方式(使用glide,picaso等),及设置为自己xml自定义布局
使用示例:
import android.app.Activity
import android.graphics.Color
import android.os.Bundle
import com.facebook.drawee.backends.pipeline.Fresco
import kotlinx.android.synthetic.main.activity_main.*
class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
Fresco.initialize(application)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var datas = ArrayList<BannerEnt>()
datas.add(BannerEnt("https://csdnimg.cn/feed/20201014/e5d5833cb9cf1355dfcd2e31af0b9c0e.jpg", 0))
datas.add(BannerEnt("https://csdnimg.cn/feed/20201013/1abff9365808a81798e6ab53c1101146.jpg", 1))
datas.add(BannerEnt("https://csdnimg.cn/feed/20201013/9bc6b56dd7ae5771d192b764b7901aa3.png", 2))
banner.setRatio(0.5f)
banner.setNavType(2)
banner.setNavColor(Color.parseColor("#ffff0000"), Color.parseColor("#ff00ff00"))
banner.setDatas(datas)
}
}