背景
说到图片轮播,之前写过一篇文章《造轮子:android自定义专属广告轮播控件》,不过当时是采用ViewFlipper实现图片轮播的,最近开始研究Rxjava技术,发现有个interval的方法,觉得很实用,就打算去实战写一个东西来玩玩。就笔者目前接触的项目,发现图片轮播这种功能,应用非常之多,由此笔者就构想用Rxjava + ViewPaper去写一个通用能自定义能扩展的图片轮播框架。
目的
开源项目,打造一个好用的图片轮播框架。
思路
相信大家之前都用过ViewPaper,所以此处不多做解释,不清楚的童鞋可以百度或谷歌。Rxjava,之前写过两篇文章:《Rxjava实践之路[入门篇]》、《Rxjava实践之路[初级篇]》,不太清楚的童鞋可以去看看,或者百度查其他文章了解了解。大体实现思路如下:
- ViewPaper实现滑动切换页面
- Rxjava定时使ViewPaper切换页面
有两个大体思路,接下来我们从细节出发,首先我们考虑以下两个问题:
- 要不要循环?
- 要不要自动轮播?
接着就衍生以下几种可能性:
- 不循环(肯定不轮播,只能手动切换图片)
- 循环
- 自动轮播
- 手动切换图片
然后考虑指示器问题,会衍生以下几个问题:
- 指示器如何摆放?
- 指示器图标是否需要自定义?
由此诞生以下几种可能性:
- 指示器摆放位置产生靠左,靠右,靠中三种选择
- 指示器肯定需要能自定义图标,满足大众选择要求嘛~
接着考虑怎么去实现以上需求,首先我们将ViewPager切换页面划分为两种:
- 不循环:此时所做工作,跟平常使用ViewPager切换页面无区别,无特殊处理。
- 循环: 用过ViewPager的童鞋都清楚,ViewPager怎么可以循环呀?这里做了一个巧妙地工作,这也是ViewPaper实现自动轮播原理所在。这块做详细说明,重点来了,大家擦亮眼睛看清啦~~
ViewPager实现自动轮播原理说明:
假如现在有三张图需要自动轮播,图1,图2,图3。那轮播View集合就需要增加两张图,在原图3后面增加图1,在原图1前增加图3,处理过后的轮播View集合顺序是这样的图3、图1、图2、图3、图1。此刻有些童鞋看着有点懵,这样处理有啥用呀?
1132780-253900d649118aa0.jpg处理前后比对:
处理前轮播View集合顺序:图1、图2、图3。
处理后轮播View集合顺序:新图3、原图1、原图2、原图3、新图1。
当向右滑动ViewPager,滑动到最后一个位置即新图1,此时做一个巧妙地跳转,ViewPager有个setCurrentItem(int item, boolean smoothScroll)方法,将smoothScroll置为false,跳转到原图1,因为两图是一样的图,而又看不到滑动效果,所以感觉没变一样。
当向左滑动ViewPager,滑动到第一个位置即新图3,同样调用setCurrentItem(item,false)等方法跳转到原图3,至此就达到了循环效果了。
童鞋们,是不是很简单?思路分析到此,接下来代码撸起来~~~
代码解析
首先我们需要写一个xml布局文件,里面包括ViewPager(用于展示图片)、指示器(显示当前图片在第几张)、标题(这个作为可选项),代码如下:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<android.support.v4.view.ViewPager
android:id="@+id/vp_cycle"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/ly_cycle_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:paddingBottom="24dp"
android:gravity="center"
android:orientation="horizontal" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/ly_cycle_indicator"
android:orientation="vertical">
<TextView
android:id="@+id/tv_cycle_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="20sp" />
</LinearLayout>
</RelativeLayout>
接着写初始化view操作,所做的工作包括初始化一个View,初始化该View需要用到的控件并将该View添加为当前轮播自定义View的子View。代码如下:
/**
* 初始化view
* @author leibing
* @createTime 2016/09/20
* @lastModify 2016/09/20
* @param context 上下文
* @return
*/
private void initView(Context context) {
// 指定布局
View view = LayoutInflater.from(context).inflate(R.layout.widget_cycle_view, null);
// findView
mViewPager = (ViewPager) view.findViewById(R.id.vp_cycle);
mTitle = (TextView) view.findViewById(R.id.tv_cycle_title);
mIndicatorLy = (LinearLayout) view.findViewById(R.id.ly_cycle_indicator);
// 添加view到轮播view
this.addView(view);
}
然后给该轮播自定义View添加数据源,主要做以下工作:
- 添加轮播View
- 添加指示器
- 设置轮播适配器
- 开始订阅自动轮播
代码如下:
/**
* 设置数据源
* @author leibing
* @createTime 2016/09/20
* @lastModify 2016/09/20
* @param mData 轮播view数据源
* @param defaultPosition 默认显示位置
* @param defaultImage 默认占位图(图片未加载出来前)
* @param listener 轮播监听
* @return
*/
public void setData(List<CycleModel> mData, int defaultPosition,
Drawable defaultImage, CycleViewListener listener){
// 设置轮播view数据源
this.mData = mData;
// 如果数据源不存在或者其大小为0则设置当前布局为不可见
if (mData == null || mData.size() == 0){
this.setVisibility(View.GONE);
return;
}
int size = mData.size();
// 如果默认显示位置超过轮播view数目则默认位置从第一个位置开始
if (defaultPosition >= size)
defaultPosition = 0;
// 轮播view数目为1,则不需要循环
if (size == 1)
isCycle = false;
// 清除mViews数据
mViews.clear();
// 添加轮播view
if (isCycle) {
// 添加轮播图View,数量为集合数+2
// 将最后一个View添加进来
mViews.add(getCycleView(getContext(), mData.get(size - 1).getUrl(), defaultImage));
for (int i = 0; i < size; i++) {
mViews.add(getCycleView(getContext(), mData.get(i).getUrl(), defaultImage));
}
// 将第一个View添加进来
mViews.add(getCycleView(getContext() , mData.get(0).getUrl(), defaultImage));
} else {
// 只添加对应数量的View
for (int i = 0; i < size; i++) {
mViews.add(getCycleView(getContext(), mData.get(i).getUrl(), defaultImage));
}
}
// 设置轮播监听
cycleViewListener = listener;
// 初始化指示器
initIndicators(size, getContext());
// 设置指示器
setIndicator(defaultPosition);
// 设置适配器
setAdapter(mViews, cycleViewListener, size);
// 如果已经开始轮播订阅,则取消轮播订阅
cancelSubscription();
// 开始轮播
startWheel(size);
}
订阅轮播代码如下:
/**
* 开始轮播
* @author leibing
* @createTime 2016/09/21
* @lastModify 2016/09/21
* @param size 轮播view数目
* @return
*/
private void startWheel(int size){
if (size < 2 || !isCycle()){
// 取消轮播
setWheel(false);
return;
}
// 设置轮播
setWheel(true);
// 开始轮播
mSubscription = Observable.interval(delay, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
.subscribe(new Action1<Long>() {
@Override
public void call(Long aLong) {
if (isWheel && isHasWheel) {
mCurrentPosition++;
if (mViewPager != null)
mViewPager.setCurrentItem(mCurrentPosition, false);
}
}
});
}
ViewPager在页面切换做了相关处理,思路里面已经讲了,而且注释也比较清楚,代码如下:
@Override
public void onPageSelected(int position) {
int max = mViews.size() - 1;
mCurrentPosition = position;
if (isCycle()) {
if (position == 0) {
// 滚动到mView的1个(界面上的最后一个),将mCurrentPosition设置为max - 1
mCurrentPosition = max - 1;
} else if (position == max) {
// 滚动到mView的最后一个(界面上的第一个),将mCurrentPosition设置为1
mCurrentPosition = 1;
}
position = mCurrentPosition - 1;
}
setIndicator(position);
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == 0 && isCycle()) { // viewPager滚动结束
//跳转到第mCurrentPosition个页面(没有动画效果,实际效果页面上没变化)
mViewPager.setCurrentItem(mCurrentPosition, false);
}
}
然后就做了一些指示器自定义处理,如指示器位置和指示器图标,代码如下:
/**
* 设置指示器图片
* @author leibing
* @createTime 2016/09/20
* @lastModify 2016/09/20
* @param select 选中时的图片
* @param unselect 未选中时的图片
* @return
*/
public void setIndicators(int select, int unselect) {
mIndicatorSelected = select;
mIndicatorUnselected = unselect;
}
/**
* 指示器靠右显示
* @author leibing
* @createTime 2016/09/21
* @lastModify 2016/09/21
* @param paddingRight 指示器距右边内边距
* @param paddingBottom 指示器距底部内边距
* @return
*/
public void setAlignParentRight(int paddingRight, int paddingBottom){
if (mIndicatorLy == null)
return;
// 设置为靠右
mIndicatorLy.setGravity(Gravity.RIGHT);
// 设置内边距
mIndicatorLy.setPadding(0,0,paddingRight,paddingBottom);
// 重新布局
mIndicatorLy.requestLayout();
}
/**
* 指示器靠右显示
* @author leibing
* @createTime 2016/09/21
* @lastModify 2016/09/21
* @param paddingLeft 指示器距左边内边距
* @param paddingBottom 指示器距底部内边距
* @return
*/
public void setAlignParentLeft(int paddingLeft, int paddingBottom){
// 设置为靠左
mIndicatorLy.setGravity(Gravity.LEFT);
// 设置内边距
mIndicatorLy.setPadding(paddingLeft, 0, 0, paddingBottom);
// 重新布局
mIndicatorLy.requestLayout();
}
/**
* 指示器设置居中显示
* @author leibing
* @createTime 2016/09/21
* @lastModify 2016/09/21
* @param paddingBottom 指示器距底部内边距
* @return
*/
public void setAlignParentCenter(int paddingBottom){
if (mIndicatorLy == null)
return;
// 设置为居中
mIndicatorLy.setGravity(Gravity.CENTER);
// 设置内边距
mIndicatorLy.setPadding(0, 0, 0, paddingBottom);
// 重新布局
mIndicatorLy.requestLayout();
}
最后,对自动轮播时手动滑动做了优化处理,当手指按下或者滑动的过程中停止轮播,手指离开屏幕开始轮播,代码如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_DOWN:
// 手指按下或者滑动的过程中停止轮播
setWheel(false);
break;
case MotionEvent.ACTION_UP:
// 手指离开屏幕开始轮播
setWheel(true);
break;
}
return super.dispatchTouchEvent(ev);
}
注意事项
当前页面不再使用该自定义图片轮播,记得在Activity onDestory方法中取消订阅(为了避免内存泄漏问题),只需调用cancelSubscription()方法,方法代码如下:
/**
* 取消轮播订阅
* @author leibing
* @createTime 2016/09/22
* @lastModify 2016/09/22
* @param
* @return
*/
public void cancelSubscription(){
if (mSubscription != null){
mSubscription.unsubscribe();
mSubscription = null;
}
}
运行效果图如下:
LbaizxfCycleView.gif笔者文笔太糟,欢迎吐槽,如有不对之处,请留言指点~~
呼吁大家动手实践,一切将会变得很容易~~~
项目地址:LbaizxfCycleView
关于作者
- QQ:872721111
- Email:leibing1989@126.com
- Github:leibing@github
- 简书:leibing@jianshu
- 掘金:leibing@juejin
网友评论