虽然网上这种自定义ViewPager实现的banner挺多,但还是自己动手写下会好很多。权当练手吧。
一、效果预览
二、需求分析
1.重写ViewPager控件实现bannerView
2.需要定时轮播
3.需要无限轮播
4.内存优化
5.ViewPager自身切换速率太快,需要重新设置
三、自定义View套路代码
直接继承自ViewPager
public class BannerViewPager extends ViewPager {
//内存优化,复用的View.
private SparseArray<View> mConvertViews;
public BannerViewPager(Context context) {
this(context, null);
}
public BannerViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
//内存优化,缓存页面数据,下面会讲到
mConvertViews = new SparseArray<>();
}
}
四、采用Adapter设计模式
原始的ViewPager就是采用了adapter设计模式,方便用户使用,所以这里我们也仿照源码思想,采用adapter设计模式。
新建一个BannerAdapter类:
public abstract class BannerAdapter {
//Observable,观察者模式,下面讲到
private BannerViewPager.AdapterDataObservable mObservable = new BannerViewPager.AdapterDataObservable();
//返回页面的View
public abstract View getView(int position);
//返回BannerView的页面数量,如果是无限轮播,记住要返回真是页面数量。
public abstract int getCount();
//ViewPager页面切换特效,方便自定义,返回null采用默认特效
public Transformer getTransformer() {
return null;
}
//************************** 观察者设计模式 **************************
public void registerAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
public void unregisterAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.unregisterObserver(observer);
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
}
BannerViewPager中定义setAdapter方法:
public void setAdapter(BannerAdapter adapter) {
if (mBannerAdapter != null) {
mBannerAdapter.unregisterAdapterDataObserver(mObserver);
}
this.mBannerAdapter = adapter;
if (mBannerAdapter == null) {
throw new IllegalArgumentException("BannerAdapter不能为null");
}
mBannerAdapter.registerAdapterDataObserver(mObserver);
//设置切换动画
if (mBannerAdapter.getTransformer() != null) {
mTransformer = mBannerAdapter.getTransformer();
mTransformer.bind(this);
}
mBannerPagerAdapter = new BannerPagerAdapter();
setAdapter(mBannerPagerAdapter);
setCurrentItem(mBannerAdapter.getCount());
}
在这个方法中,我们主要做了这几件事:
1.观察者设计模式的处理。
2.bannerViewPager翻页动画。
3.给ViewPager设置真实的adapter:BannerPagerAdapter,下面就看下这个Adapter内部类。
在BannerViewPager中定义BannerPagerAdapter类:
private class BannerPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return mBannerAdapter.getCount() == 1 ? 1 : Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
//先转化为实际position
final int realPosition = position % mBannerAdapter.getCount();
View bannerItemView = mBannerAdapter.getView(realPosition);
container.addView(bannerItemView);
//点击事件
bannerItemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onBannerItemClick(realPosition);
}
}
});
return bannerItemView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
}
其中有页面点击事件处理,很简单的回调
public BannerItemClickListener mItemClickListener;
public interface BannerItemClickListener {
void onBannerItemClick(int position);
}
/**
* 条目点击事件
*/
public void setOnItemClickListener(BannerItemClickListener listener) {
this.mItemClickListener = listener;
}
这里说下无限轮播的思路,奥妙都在BannerPagerAdapter 这个类中,
getCount方法返回Integer.MAX_VALUE,代表ViewPager有2^31-1个页面
instantiateItem方法中根据postion返回每个页面的View,postion最大为Integer.MAX_VALUE-1,而实际页面数只有有限的几个,所以需要做下转换final int realPosition = position % mBannerAdapter.getCount();
这样处理后,假设有实际页面ABC,最终的效果就是[position=0,页面A] [position=1,页面B] [position=2,页面C] [position=3,页面A] [position=4,页面B] [position=5,页面C] [position=6,页面A] [position=7,页面B] ...
五、自动轮播
自动轮播我们采用handler,handler.postDelayed(Runnable r, long delayMillis),同时传入的runable的run方法里,再次postDelayed调用自身runnable方法,形成一个递归。
//初始化handler
@SuppressLint("HandlerLeak")
private void initHandler() {
mHandler = new Handler();
}
//定义一个runnable,并在run方法内再次mHandler.postDelayed自身
private final Runnable task = new Runnable() {
@Override
public void run() {
setCurrentItem(getCurrentItem() + 1);
mHandler.postDelayed(task, mCutDownTime);
}
};
接着定义开始和停止滚动的两个方法,其实就是控制handler的执行和停止任务:
public void startScroll() {
if (mBannerAdapter == null) {
return;
}
boolean scrollable = mBannerAdapter.getCount() != 1;
if (scrollable && mHandler != null) {
mHandler.postDelayed(task, mCutDownTime);
}
}
public void stopScroll() {
if (mBannerAdapter == null) {
return;
}
if (mHandler != null) {
mHandler.removeCallbacks(task);
}
}
mCutDownTime就是轮播间隔时间,当然也得要提供给用户api控制这个间隔
private int mCutDownTime = 5000;
/**
* 设置每个条目切换时间
*/
public void setCutDownTime(int millis) {
this.mCutDownTime = millis;
}
到此,自动轮播处理完毕。
六、自定义Scroller改变ViewPager默认切换速率
通过查看源码我们发现,ViewPager的滚动内部采用scroller控制,那么我们自己定义一个scroller将他自带的替换掉即可。
public class BannerScroller extends Scroller {
private int mDuration = 1000;
public BannerScroller(Context context) {
super(context);
}
public void setScrollerDuration(int duration) {
this.mDuration = duration;
}
@Override
public void startScroll(int startX, int startY, int dx, int dy) {
super.startScroll(startX, startY, dx, dy,mDuration);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, mDuration);
}
}
mDuration即控制滑动时间的变量。
在BannerViewPager的构造方法中,替换默认mScroller为我们自定义的Scroller,不过遗憾的是ViewPager并没有提供相关api,且mScroller为私有的,我们这里只能采取一些手段了:反射。
public BannerViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
mConvertViews = new SparseArray<>();
//反射修改ViewPager默认的滚动速率
try {
final Field field = ViewPager.class.getDeclaredField("mScroller");
mBannerScroller = new BannerScroller(context);
field.setAccessible(true);
field.set(this, mBannerScroller);
} catch (Exception e) {
e.printStackTrace();
}
initHandler();
}
到这里,自定义ViewPager也算勉强可以用了,不过还有不少值得优化的地方。
七、内存优化
回到BannerPagerAdapter这个类中,instantiateItem方法,每次获取页面,直接调用mBannerAdapter.getView(realPosition);
,我们的轮播图条目数可以有Integer.MAX_VALUE-1个的,这个方法就要调用无数次,页面就要初始化无数次,而实际的页面只有有限的几个,能不能优化呢,将初始化后的页面缓存起来即可!
//用SparseArray缓存,键为postion,真实的postion;值为页面数据。
private SparseArray<View> mConvertViews;
public View getConvertView(int position) {
final View convertView = mConvertViews.get(position, null);
if (convertView == null || convertView.getParent() != null) {
//健壮性判断,如果缓存的convertView有它的parent,那么返回null。
return null;
}
return convertView;
}
//内存优化后的BannerPagerAdapter
private class BannerPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return mBannerAdapter.getCount() == 1 ? 1 : Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
//先转化为实际position
final int realPosition = position % mBannerAdapter.getCount();
View bannerItemView = getConvertView(realPosition);
//先从缓存中拿,如果拿不到,那么就重新初始化
if (bannerItemView == null) {
bannerItemView = mBannerAdapter.getView(realPosition);
}
container.addView(bannerItemView);
bannerItemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mItemClickListener != null) {
mItemClickListener.onBannerItemClick(realPosition);
}
}
});
return bannerItemView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
//页面从viewpager移除的同时,缓存到mConvertViews中
mConvertViews.put(position % mBannerAdapter.getCount(), (View) object);
}
}
八、bug修复
在使用中,存在这样一个现象,比如嵌套在RecyclerView中使用,如果该bannerViewPager被滑出屏幕再滑进屏幕,ViewPager的第一次切换没有动画,很生硬。
查看源码:
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mFirstLayout = true;
}
这个onAttachedToWindow方法中,将mFirstLayout 置为true,在切换时,会先判断该字段,只有mFirstLayout =false时才启用滑动动画,所以重写该方法即可,同样mFirstLayout 为私有,采取反射
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
try {
//缓存反射字段
if (mFirstLayoutField == null) {
//反射耗时,缓存一下
mFirstLayoutField = ViewPager.class.getDeclaredField("mFirstLayout");
}
mFirstLayoutField.setAccessible(true);
mFirstLayoutField.set(this,false);
} catch (Exception e) {
e.printStackTrace();
}
startScroll();
}
//离开屏幕,暂停轮播。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopScroll();
}
九、处理点击暂停
手指在触摸或滑动bannerView的时候不应自动轮播,需要停止。
开始我想重写onTouchEvent或onInterceptTouchEvent,都没能达到效果,于是我就翻看了其他BannerView的处理,发现他们都是在dispatchTouchEvent中处理的。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int action = ev.getAction();
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL
|| action == MotionEvent.ACTION_OUTSIDE) {
startScroll();
} else if (action == MotionEvent.ACTION_DOWN) {
stopScroll();
}
return super.dispatchTouchEvent(ev);
}
后来我分析了一下事件分发:
对于onTouchEvent,
ACTION_DOWN 不经过此方法,向下传递到它的子view时,子view的onTouchEvent返回true(bannerViewPager的子view设置了点击事件),因此事件不再回传给该BannerViewPager。
ACTION_MOVE,ACTION_UP经过此方法,子view不处理这两种事件,交给该BannerViewPager处理拖拽滑动事件。
对于onInterceptTouchEvent,
ACTION_DOWN 经过此方法
ACTION_MOVE ACTION_UP不经过此方法,因为在move和up的时候,onTouchEvent处理了拖拽滑动事件,此时mFirstTouchTarget被置为null, dispatchTouchEvent处理分发事件时,就不走onInterceptTouchEvent方法了。
十、加入切换动画
我使用了仿魅族商店的切换动画,具体原理看这里
https://www.jianshu.com/p/e67aa68d2766
算法什么的我就无耻的直接拿过来用了
定义接口:
public interface Transformer {
void bind(BannerViewPager viewPager);
int getChildDrawingOrder(int childCount, int n);
}
仿魅族切换动画
public class MZTransformImpl implements Transformer{
//中间放大系数
private float mScaleMax = 1.0f;
//两边缩小系数
private float mScaleMin = 0.9f;
//重叠部分
private float mCoverWidth = 80f;
private ArrayList<Integer> childCenterXAbs = new ArrayList<>();
private SparseArray<Integer> childIndex = new SparseArray<>();
private BannerViewPager mBannerViewPager;
@Override
public void bind(BannerViewPager viewPager) {
this.mBannerViewPager = viewPager;
mBannerViewPager.setPageTransformer(true, new SPageTransformer());//默认调用了 setChildrenDrawingOrderEnabledCompat(true);使得getChildDrawingOrder起作用
mBannerViewPager.setClipToPadding(false);
mBannerViewPager.setOverScrollMode(ViewPager.OVER_SCROLL_NEVER);
}
public int getChildDrawingOrder(int childCount, int n) {
if (n == 0 || childIndex.size() != childCount) {
childCenterXAbs.clear();
childIndex.clear();
int viewCenterX = getViewCenterX(mBannerViewPager);
for (int i = 0; i < childCount; ++i) {
int indexAbs = Math.abs(viewCenterX - getViewCenterX(mBannerViewPager.getChildAt(i)));
//两个距离相同,后来的那个做自增,从而保持abs不同
if (childIndex.get(indexAbs) != null) {
++indexAbs;
}
childCenterXAbs.add(indexAbs);
childIndex.append(indexAbs, i);
}
Collections.sort(childCenterXAbs);//1,0,2 0,1,2
}
//那个item距离中心点远一些,就先draw它。(最近的就是中间放大的item,最后draw)
return childIndex.get(childCenterXAbs.get(childCount - 1 - n));
}
private int getViewCenterX(View view) {
int[] array = new int[2];
view.getLocationOnScreen(array);
return array[0] + view.getWidth() / 2;
}
class SPageTransformer implements ViewPager.PageTransformer {
private float reduceX = 0.0f;
private float itemWidth = 0;
private float offsetPosition = 0f;
@Override
public void transformPage(View view, float position) {
if (offsetPosition == 0f) {
float paddingLeft = mBannerViewPager.getPaddingLeft();
float paddingRight = mBannerViewPager.getPaddingRight();
float width = mBannerViewPager.getMeasuredWidth();
offsetPosition = paddingLeft / (width - paddingLeft - paddingRight);
}
float currentPos = position - offsetPosition;
if (itemWidth == 0) {
itemWidth = view.getWidth();
//由于左右边的缩小而减小的x的大小的一半
reduceX = (2.0f - mScaleMax - mScaleMin) * itemWidth / 2.0f;
}
if (currentPos <= -1.0f) {
view.setTranslationX(reduceX + mCoverWidth);
view.setScaleX(mScaleMin);
view.setScaleY(mScaleMin);
} else if (currentPos <= 1.0) {
float scale = (mScaleMax - mScaleMin) * Math.abs(1.0f - Math.abs(currentPos));
float translationX = currentPos * -reduceX;
if (currentPos <= -0.5) {//两个view中间的临界,这时两个view在同一层,左侧View需要往X轴正方向移动覆盖的值()
view.setTranslationX(translationX + mCoverWidth * Math.abs(Math.abs(currentPos) - 0.5f) / 0.5f);
} else if (currentPos <= 0.0f) {
view.setTranslationX(translationX);
} else if (currentPos >= 0.5) {//两个view中间的临界,这时两个view在同一层
view.setTranslationX(translationX - mCoverWidth * Math.abs(Math.abs(currentPos) - 0.5f) / 0.5f);
} else {
view.setTranslationX(translationX);
}
view.setScaleX(scale + mScaleMin);
view.setScaleY(scale + mScaleMin);
} else {
view.setScaleX(mScaleMin);
view.setScaleY(mScaleMin);
view.setTranslationX(-reduceX - mCoverWidth);
}
}
}
}
回到BannerViewPager类中,setAdapter方法内设置切换动画
public void setAdapter(BannerAdapter adapter) {
```
//设置切换动画
if (mBannerAdapter.getTransformer() != null) {
mTransformer = mBannerAdapter.getTransformer();
mTransformer.bind(this);
}
```
}
重写getChildDrawingOrder控制绘制顺序
/**
* 控制子View的绘制顺序
*/
@Override
protected int getChildDrawingOrder(int childCount, int i) {
if (mTransformer != null) {
return mTransformer.getChildDrawingOrder(childCount, i);
}
return super.getChildDrawingOrder(childCount, i);
}
注意,如果要采用仿魅族切换样式,在BannerViewpager需要设置paddingLeft和paddingRight,在xml中定义即可。
十一、最后讲下观察者设计模式
源码中Adaper里很多到用到了观察者模式,Adapter中数据变化,调用notifyDataSetChanged,那么对应的View也会变化,这都是有一定模板套路的,也可参考上一篇https://www.jianshu.com/p/d75edebb6c8f的讲解。
这里就总结下套路:
首先要弄明白,谁是观察者(observer),谁是被观察者(observable)
记住一点:被观察者发生变化,观察者就会响应变化。
举个例子,手机是被观察者吧,人就是观察者,手机来微信了,就是被观察者发生变化,我们会很自然的去打开微信查看,这就是观察者响应变化。
类比一下,adapter就是被观察者,BannerViewpager就是观察者,因为adapter数据变化,BannerViewpager得要响应改变ui。
在代码世界里,这种响应是如何做到的呢,为什么observer会响应observable的变化,说白了就是observable中持有了observer的引用,observable发生变化后,再主动调用observer相关变化的方法即可。
开始上代码,BannerViewPager中:
private final BannerDataObserver mObserver = new BannerDataObserver();
static class AdapterDataObservable extends Observable<AdapterDataObserver> {
public void notifyChanged() {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
}
public static abstract class AdapterDataObserver {
public void onChanged() {
}
}
private class BannerDataObserver extends AdapterDataObserver{
@Override
public void onChanged() {
mBannerPagerAdapter.notifyDataSetChanged();
}
}
BannerAdapter中:
private BannerViewPager.AdapterDataObservable mObservable = new BannerViewPager.AdapterDataObservable();
//************************** 观察者设计模式 **************************
public void registerAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.registerObserver(observer);
}
public void unregisterAdapterDataObserver(BannerViewPager.AdapterDataObserver observer) {
mObservable.unregisterObserver(observer);
}
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
在setAdapter(BannerAdapter adapter)中,将观察者注册给被观察者就完事了,实质就是依赖注入。
public void setAdapter(BannerAdapter adapter) {
if (mBannerAdapter != null) {
mBannerAdapter.unregisterAdapterDataObserver(mObserver);
}
this.mBannerAdapter = adapter;
if (mBannerAdapter == null) {
throw new IllegalArgumentException("BannerAdapter不能为null");
}
mBannerAdapter.registerAdapterDataObserver(mObserver);
...
}
这样,调用BannerAdapter的notifyDataSetChanged方法就是调用 mObservable.notifyChanged();->AdapterDataObserver.onChanged()->mBannerPagerAdapter.notifyDataSetChanged();最终ui发生改变。