源代码引用地址:https://github.com/yiyibb/Zhihu
首先来看轮播效果图

整体是个recyclerview,头部布局为banner轮播图,此处的banner是继承Framelayout实现的。代码后面会具体说明,这里我们先看轮播图的xml文件的结构,总共三部分首先是背景图片,可实现自动滑动,另外是标题,还有最下面的 圆点,都会随着图片的移动而移动。
布局文件.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/bannerLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/space_200">
<com.yiyi.zhihu.ui.widget.Banner.BannerViewPaper
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/banner_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="left"
android:maxLines="3"
android:padding="@dimen/space_16"
android:textColor="@color/colorWhite"
android:textSize="@dimen/textSize_20"
android:textStyle="bold"
tools:text="图片轮播器"
tools:textColor="@color/colorLightBlue"/>
<LinearLayout
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginBottom="@dimen/space_10"
android:layout_marginTop="@dimen/space_10"
android:gravity="center"
android:orientation="horizontal" >
</LinearLayout>
</FrameLayout>
看重点主要是BannerViewPaper,这个类仅仅继承了ViewPager,代码如下:
public class BannerViewPaper extends ViewPager {
private boolean scrollable = true;
public BannerViewPaper(Context context) {
super(context);
}
public BannerViewPaper(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
return this.scrollable && super.onTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return this.scrollable && super.onInterceptTouchEvent(ev);
}
public void setScrollable(boolean scrollable) {
this.scrollable = scrollable;
}
}
滑动事件以及点击事件设定了scrollable。
接下来看重点banner,其实这也算是自定义viewgroup了,继承了framelayout,实现了滑动以及自动播放功能。看一下代码吧,emm,代码一如既往地多,但是我会一点点来分析,踏踏实实,耐耐心心,做技术本该如此,不是吗?
public class Banner extends FrameLayout implements ViewPager.OnPageChangeListener {
private static final String TAG = "Banner";
private int indicatorSize;
private int mIndicatorWidth;
private int mIndicatorHeight;
private int mIndicatorMargin;
private int bannerStyle = BannerConfig.CIRCLE_INDICATOR;
private int delayTime = BannerConfig.TIME;
private int scrollTime = BannerConfig.DURATION;
private boolean isAutoPlay = BannerConfig.IS_AUTO_PLAY;
private boolean isScroll = BannerConfig.IS_SCROLL;
private int mIndicatorSelectedResId = R.drawable.selected_radius;
private int mIndicatorUnselectedResId = R.drawable.unselected_radius;
private int titleHeight;
private int titleBackground;
private int titleTextColor;
private int titleTextSize;
private int count = 0;
private int currentItem;
private int lastPosition = 1;
private Context mContext;
private BannerPagerAdapter mAdapter;
private List<View> imageViews;
private List<ImageView> indicatorImages;
private List imageUrls;
private List<String> titles;
private BannerViewPaper mViewPaper;
private TextView bannerTitle;
private LinearLayout indicator;
private BannerScroller mScroller;
private DisplayMetrics dm;
private ImageLoaderInterface imageLoader;
private OnBannerClickListener mOnBannerClickListener;
private ViewPager.OnPageChangeListener mOnPageChangeListener;
private WeakHandler handler = new WeakHandler();
public Banner(Context context) {
this(context, null);
}
public Banner(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public Banner(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
titles = new ArrayList<>();
imageUrls = new ArrayList();
imageViews = new ArrayList<>();
indicatorImages = new ArrayList<>();
dm = context.getResources().getDisplayMetrics();
indicatorSize = dm.widthPixels / 80;
initView(context, attrs);
}
private void initView(Context context, AttributeSet attrs) {
imageViews.clear();
View view = LayoutInflater.from(context).inflate(R.layout.banner, this, true);
mViewPaper = (BannerViewPaper) view.findViewById(R.id.viewPager);
bannerTitle = (TextView) view.findViewById(R.id.banner_title);
indicator = (LinearLayout) view.findViewById(R.id.indicator);
handleTypedArray(context, attrs);
initViewPaperScroll();
}
private void handleTypedArray(Context context, AttributeSet attrs) {
if (attrs == null) {
return;
}
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Banner);
mIndicatorWidth = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_width, indicatorSize);
mIndicatorHeight = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_height, indicatorSize);
mIndicatorMargin = typedArray.getDimensionPixelSize(R.styleable.Banner_indicator_margin, BannerConfig.PADDING_SIZE);
mIndicatorSelectedResId = typedArray.getResourceId(R.styleable.Banner_indicator_drawable_selected, R.drawable.selected_radius);
mIndicatorUnselectedResId = typedArray.getResourceId(R.styleable.Banner_indicator_drawable_unselected, R.drawable.unselected_radius);
delayTime = typedArray.getInt(R.styleable.Banner_delay_time, BannerConfig.TIME);
scrollTime = typedArray.getInt(R.styleable.Banner_scroll_time, BannerConfig.DURATION);
isAutoPlay = typedArray.getBoolean(R.styleable.Banner_is_auto_play, BannerConfig.IS_AUTO_PLAY);
titleBackground = typedArray.getColor(R.styleable.Banner_title_background, BannerConfig.TITLE_BACKGROUND);
titleHeight = typedArray.getDimensionPixelSize(R.styleable.Banner_title_height, BannerConfig.TITLE_HEIGHT);
titleTextColor = typedArray.getColor(R.styleable.Banner_title_textcolor, BannerConfig.TITLE_TEXT_COLOR);
titleTextSize = typedArray.getDimensionPixelSize(R.styleable.Banner_title_textsize, BannerConfig.TITLE_TEXT_SIZE);
typedArray.recycle();
}
private void initViewPaperScroll() {
try {
Field mField = ViewPager.class.getDeclaredField("mScroller");
mField.setAccessible(true);
mScroller = new BannerScroller(mViewPaper.getContext());
mScroller.setDuration(scrollTime);
mField.set(mViewPaper, mScroller);
} catch (Exception e) {
Log.e(TAG, e.getMessage());
}
}
public Banner isAutoPlay(boolean isAutoPlay) {
this.isAutoPlay = isAutoPlay;
return this;
}
public Banner setImageLoader(ImageLoaderInterface imageLoader) {
this.imageLoader = imageLoader;
return this;
}
public Banner setDelayTime(int delayTime) {
this.delayTime = delayTime;
return this;
}
public Banner setBannerTitles(List<String> titles) {
this.titles = titles;
bannerTitle.setText(titles.get(0));
return this;
}
public Banner setImages(List<?> imageUrls) {
this.imageUrls = imageUrls;
this.count = imageUrls.size();
return this;
}
public void update(List<?> imageUrls, List<String> titles) {
this.imageUrls.clear();
this.titles.clear();
this.imageUrls.addAll(imageUrls);
this.titles.addAll(titles);
this.count = this.imageUrls.size();
start();
}
public void update(List<?> imageUrls) {
this.imageUrls.clear();
this.imageUrls.addAll(imageUrls);
this.count = this.imageUrls.size();
start();
}
public Banner start() {
setImagesList(imageUrls);
if (isAutoPlay) {
startAutoPlay();
}
return this;
}
private void createIndicator() {//滚动的圆点
indicatorImages.clear();
indicator.removeAllViews();
for (int i = 0; i < count; ++i) {
ImageView imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(mIndicatorWidth, mIndicatorHeight);
params.leftMargin = mIndicatorMargin;
params.rightMargin = mIndicatorMargin;
if (i == 0) {
imageView.setImageResource(mIndicatorSelectedResId);
} else {
imageView.setImageResource(mIndicatorUnselectedResId);
}
indicatorImages.add(imageView);
indicator.addView(imageView, params);
}
}
private void setData() {
currentItem = 1;
if (mAdapter == null) {
mAdapter = new BannerPagerAdapter();
}
mViewPaper.setAdapter(mAdapter);
mViewPaper.setFocusable(true);
mViewPaper.setCurrentItem(currentItem);
mViewPaper.addOnPageChangeListener(this);
if (isScroll && count > 1) {
mViewPaper.setScrollable(true);
} else {
mViewPaper.setScrollable(false);
}
}
private void setImagesList(List<?> imageUrls) {
if (imageUrls == null || imageUrls.size() <= 0) {
Log.e(TAG, "please set the images data");
return;
}
imageViews.clear();
createIndicator();//滚动小圆球
for (int i = 0; i <= count + 1; ++i) {
View imageView = null;
if (imageLoader != null) {
imageView = imageLoader.creteImageView(mContext);
}
if (imageView == null) {
imageView = new ImageView(mContext);
}
((ImageView)imageView).setScaleType(ImageView.ScaleType.CENTER_CROP);
Object url = null;
// 无限轮播实现原理
if (i == 0) {
url = imageUrls.get(count - 1);
} else if (i == count + 1) {
url = imageUrls.get(0);
} else {
url = imageUrls.get(i - 1);
}
imageViews.add(imageView);
if (imageLoader != null) {
imageLoader.displayImage(mContext, url, imageView);
} else {
Log.e(TAG, "please set image loader");
}
}
setData();
}
public void startAutoPlay() {
handler.removeCallbacks(task);
handler.postDelayed(task, delayTime);
}
public void stopAutoPlay() {
handler.removeCallbacks(task);
}
private final Runnable task = new Runnable() {
@Override
public void run() {
if (count > 1 && isAutoPlay) {
currentItem = currentItem % (count + 1) + 1;
if (currentItem == 1) {
mViewPaper.setCurrentItem(currentItem);
handler.postDelayed(task, delayTime);
Log.i("困了1",mViewPaper.getCurrentItem()+"");
} else if (currentItem == count+1) {
//mViewPaper.setCurrentItem(currentItem,false);
handler.post(task);
Log.i("困了1",mViewPaper.getCurrentItem()+"");
} else {
mViewPaper.setCurrentItem(currentItem);
handler.postDelayed(task, delayTime);
Log.i("困了2",mViewPaper.getCurrentItem()+"");
}
}
}
};
/**
* 返回真实的位置
*
* @param position
* @return 下标从0开始
*/
public int toRealPosition(int position) {
int realPosition = (position - 1) % count;
if (realPosition < 0) {
realPosition += count;
}
return realPosition;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (isAutoPlay) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_OUTSIDE) {
startAutoPlay();
} else if (action == MotionEvent.ACTION_DOWN) {
stopAutoPlay();
}
}
return super.dispatchTouchEvent(ev);
}
class BannerPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return imageViews.size();
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, final int position) {
container.addView(imageViews.get(position));
View view = imageViews.get(position);
if (mOnBannerClickListener != null) {
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
mOnBannerClickListener.OnBannerClick(toRealPosition(position));
}
});
}
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View)object);
}
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
if (mOnPageChangeListener != null) {//不执行
mOnPageChangeListener.onPageScrolled(position, positionOffset, positionOffsetPixels);
Log.i("梦醒","对象为空1");
}
}
@Override
public void onPageSelected(int position) {
if (mOnPageChangeListener != null) {//不执行
mOnPageChangeListener.onPageSelected(position);
Log.i("梦醒","对象为空2");
}
indicatorImages.get((lastPosition - 1 + count) % count).setImageResource(mIndicatorUnselectedResId);
indicatorImages.get((position - 1 + count) % count).setImageResource(mIndicatorSelectedResId);
lastPosition = position;
if (position == 0) position = count;
if (position > count) position = 1;
bannerTitle.setText(titles.get(position - 1));
}
@Override
public void onPageScrollStateChanged(int state) {
if (mOnPageChangeListener != null) {
mOnPageChangeListener.onPageScrollStateChanged(state);
}
currentItem = mViewPaper.getCurrentItem();
switch (state) {
/* SCROLL_STATE_DRAGGING(1)表示用户手指“按在屏幕上并且开始拖动”的状态(手指按下但是还没有拖动的时候还不是这个状态,
只有按下并且手指开始拖动后log才打出。)
SCROLL_STATE_IDLE(0)滑动动画做完的状态。
SCROLL_STATE_SETTLING(2)在“手指离开屏幕”的状态。*/
case 0://No operation
/* if (currentItem == 0) {
mViewPaper.setCurrentItem(count,false);
} else if (currentItem == count + 1) {
mViewPaper.setCurrentItem(1, false);
}
Log.i("梦醒0",mViewPaper.getCurrentItem()+"");*/
break;
case 1://start Sliding正在滑动
/*if (currentItem == count + 1) {
mViewPaper.setCurrentItem(1, false);
} else if (currentItem == 0) {
mViewPaper.setCurrentItem(count, false);
}
Log.i("梦醒1",mViewPaper.getCurrentItem()+"");*/
break;
case 2://end Sliding
/*if (currentItem == 0) {
mViewPaper.setCurrentItem(count,false);
} else if (currentItem == count + 1) {
mViewPaper.setCurrentItem(1, false);
}
Log.i("梦醒2",mViewPaper.getCurrentItem()+"");*/
break;
}
}
public void setOnPageChangeListener(ViewPager.OnPageChangeListener onPageChangeListener) {
mOnPageChangeListener = onPageChangeListener;
}
public Banner setOnBannerClickListener(OnBannerClickListener listener) {
this.mOnBannerClickListener = listener;
return this;
}
public interface OnBannerClickListener {
public void OnBannerClick(int position);
}
}
找关键,挑重点,重要的几个方法就这么多
initView();
startAutoPlay();
task;
介绍一下整体流程,说到底banner的实现使用就是viewpager,利用定时让viewpager自动播放,难点是无限轮播,这里我也只能算是看懂了代码,,但是不能理会他的精髓,直接让我写,我还是写不出来,这就是大神的能力吧。
首先initview初始化布局文件,这里面有两个方法一个是 handleTypedArray用于配置布局控件中的风格,高度长度,背景等等。另一个方法是initViewPaperScroll()这个方法是利用Java反射机制控制viewpager的滑动时间。再往后边看就是一些设定参数的方法,没啥可讲的。
后面重点是start方法,这个方法里面先是初始化了数据。关键代码如下:
for (int i = 0; i <= count + 1; ++i) {
View imageView = null;
if (imageLoader != null) {
imageView = imageLoader.creteImageView(mContext);
}
if (imageView == null) {
imageView = new ImageView(mContext);
}
((ImageView)imageView).setScaleType(ImageView.ScaleType.CENTER_CROP);
Object url = null;
// 无限轮播实现原理
if (i == 0) {
url = imageUrls.get(count - 1);
} else if (i == count + 1) {
url = imageUrls.get(0);
} else {
url = imageUrls.get(i - 1);
}
imageViews.add(imageView);
if (imageLoader != null) {
imageLoader.displayImage(mContext, url, imageView);
} else {
Log.e(TAG, "please set image loader");
}
}
这个轮播图片总数为5,也就是count为5。画了一张草图

图上面是从网络上获取到的url集合,图下面是viewpager对应的imageview的六张图片。下面来看runable延时任务,每隔一段时间就会执行一次,但是由于这里是无限轮播,所以会有一些特殊处理
private final Runnable task = new Runnable() {
@Override
public void run() {
if (count > 1 && isAutoPlay) {
currentItem = currentItem % (count + 1) + 1;
if (currentItem == 1) {
mViewPaper.setCurrentItem(currentItem);
handler.postDelayed(task, delayTime);
} else if (currentItem == count+1) {
handler.post(task);
} else {
mViewPaper.setCurrentItem(currentItem);
handler.postDelayed(task, delayTime);
}
}
}
};
这里其实是一个循环,因为 currentItem每次都会除以6取余加1,初始化currentItem为1,需要注意的是 currentItem是从imageviews中取的值。 currentItem会按照1—>2—>3—>4—>5—>6—>1这样循环下去。
当照片为imageviews中的最后一张时,task任务会直接执行下一次任务不做延时,也就是直接蹦到imageviews的第一张。这时候您如果仔细看的话会发现imageviews的第0张图片并没有什么卵用啊,其实这个问题是隐藏的,刚开始我也没注意到。当currentItem为1时,也就是imageviews中的第一张照片,这时候用户要是向左滑动,就会蹦到imageviews的第0张图片。这时候再来看,按照正常逻辑走 currentItem下一个又会变成1,不得不佩服作者的逻辑,这种可以被算上算法了吧,左右的操作都在这个循环里。
Tip:我对作者的源代码稍稍改变了一下,因为他这里有点小小的问题,viewpager在自动滑动的情况下,当用户向左滑动viewpager一直到倒数第一张图片,这时候用户松手,轮播会直接跳转到第二张图片然后继续滑动。我修改了onPageScrollStateChanged和task两个方法,您可以仔细研究一下作者的源码。https://github.com/yiyibb/Zhihu
还有一些次要的方法:
createIndicator:用于底部圆点的滚动,这个圆点滚动效果其实是在onPageSelected时候开始变化的,道理比较简单,也是跟上面一样像一个循环。
onPageScrollStateChanged:这个方法原作者是写了一些代码的,针对的也是用户滑动事件,简单说说这个方法对应着三种状态
SCROLL_STATE_DRAGGING(1)表示用户手指“按在屏幕上并且开始拖动”的状态(手指按下但是还没有拖动的时候还不是这个状态,只有按下并且手指开始拖动后log才打出。)
SCROLL_STATE_IDLE(0)滑动动画做完的状态。
SCROLL_STATE_SETTLING(2)在“手指离开屏幕”的状态。
源代码作者写的代码我感觉是非必要的,已经注释掉了,也可能是自己没能领会到吧。
dispatchTouchEvent:这个是触摸事件,用于拦截用户的滑动事件,没啥可讲的
OnBannerClickListener:监听回调接口
感兴趣的可以关注我最新开的公众号,重在分享!!微信搜索 开发Android的小学生
