前言:
这是自己实现的一个轮播图控件,下面的文章是记录自己的开发过程。这个项目我已经做成gitHub的开源库了,可供大家方便使用
gitHub地址
一、ViewPager源码分析
1. setAdapter
我们从viewPager的setAdapter()方法入手
public void setAdapter(@Nullable PagerAdapter adapter) {
if (mAdapter != null) {
//观察者模式
mAdapter.setViewPagerObserver(null);
//...
}
//我们重点关注这个方法
//创建和销毁itemView
populate();
}
void populate() {
populate(mCurItem);
}
//newCurrentItem:表示当前选中的item
void populate(int newCurrentItem) {
ItemInfo oldCurInfo = null;
if (mCurItem != newCurrentItem) {
oldCurInfo = infoForPosition(mCurItem);
mCurItem = newCurrentItem;
}
if (curItem == null && N > 0) {
//进到里面,里面通过 mAdapter.instantiateItem()方法创建ItemView
curItem = addNewItem(mCurItem, curIndex);
}
if (curItem != null) {
for (int pos = mCurItem - 1; pos >= 0; pos--) {
mItems.remove(itemIndex);
//通过for循环,销毁ItemView
mAdapter.destroyItem(this, pos, ii.object);
}
}
}
ItemInfo addNewItem(int position, int index) {
ItemInfo ii = new ItemInfo();
ii.position = position;
//通过这个方法不断的创建子View
ii.object = mAdapter.instantiateItem(this, position);
ii.widthFactor = mAdapter.getPageWidth(position);
if (index < 0 || index >= mItems.size()) {
mItems.add(ii);
} else {
mItems.add(index, ii);
}
return ii;
}
由源码可以看出,ViewPager里面无论放多少页面都不会内存溢出,因为他会不断的去销毁和创建ItemView,通过mAdapter.instantiateItem(this, position);
创建和mAdapter.destroyItem(this, pos, ii.object);
这和销毁
上面的图是默认的情况,缓存左右1页,如果要自定义的话,需要调用viewPager.setOffscreenPageLimit(5);
这个方法来设置,传进去我们要缓存的页数,这时候左边5页+右边5页+自己 = 11页
2. setCurrentItem();
切换页面,执行动画
//...经过一些列的调用,最终会调到这个方法来
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//当我们切换itemView的时候,他就会通过这里去创建和销毁itemView
populate(item);
//然后滚动到要切换的页面
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
二、实现无限轮播图
实现无限录播有两种方法:
- 自定义ViewPager,继承自ViewPager
- 自定义ViewGroup,继承自ViewGroup+HorizontalScroll
为了简便,我们直接使用继承自ViewPager的方式
1. 创建自定义ViewPager
public class BannerViewPager extends ViewPager {
public BannerViewPager(@NonNull Context context) {
this(context,null);
}
public BannerViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
//为后面的Adapter模式做准备
public void setAdapter( BannerAdapter adapter) {
this.mAdapter = adapter;
//设置父类 ViewPager的adapter
setAdapter(new BannerPagerAdapter());
//这里设置Adapter之后,会不断的循环调用instantiateItem()方法,去增加ItemView
}
}
BannerViewPager类现在就已经继承自ViewPager了,但是我们得把数据放到BannerViewPager中,这个时候,我们就需要Adapter的加入了,他能把数据变成BannerViewPager需要的数据
2. 为ViewPager创建一个Adapter
/**
* 给ViewPager设置适配器
*/
public class BannerPagerAdapter extends PagerAdapter{
@Override
public int getCount() {
//为了实现无限循环
return Integer.MAX_VALUE;
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
//官方推荐这里这么写
//因为在ViewPager的addNewItem方法中,回调新增ItemView的时候ii.object = mAdapter.instantiateItem(this, position);
//返回的就是object对象,所以这里直接用view == object
return view == object;
}
/**
* 创建ViewPager条目回调的方法
* ii.object = mAdapter.instantiateItem(this, position);
* @param container 就是我们的ViewPager,上面的this就是传过来的container,就是ViewPager
* @param position
* @return 这里返回Object对象,和上面的isViewFromObject里面的逻辑对应起来了
*/
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//Adapter设计模式为了完全让用户自定义
// position 的变化 0 -> 2^31 会溢出,所以我们对总数据进行求模运算
View bannerItemView = mAdapter.getView(position%mAdapter.getCount());
//让用户去添加,实现用户的自定义
// 添加ViewPager里面
container.addView(bannerItemView);
return bannerItemView;
}
/**
* 销毁条目回调的方法
* @param container
* @param position
* @param object
*/
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
//不造成内存泄漏
object = null;
}
}
上面这个Adapter中,我们复写了getCount()方法,并把他的返回值设置成了Integer.MAX_VALUE
,这里就是为了给无限轮播做准备。
而isViewFromObject()方法里面,我们直接返回view == object
,这是官方推荐的写法,这里之所以用object与view直接比较,是因为我们创建ItemView的时候,instantiateItem()方法就是直接返回的Object对象(instantiateItem()方法的作用和回调时机上面已经分析过了)。
我们还复写了instantiateItem()方法用于创建ItemView,不过这里我们使用了Adapter设计模式,方便用户实现Adapter的自定义(自定义BannerViewPager里面放置什么样的数据)。然后我们把用户自定义的View添加到了container对象中,这个container对象其实就是我们的BannerViewPager。
destroyItem()方法就是设置如何销毁一个ItemView,我们这里是直接从我们的BannerViewPager中移除,被把object = null
,这里是为了不造成内存泄漏
3. 实现用户自定义View
在上面的Adapter中,我们使用了Adapter设计模式,目的就是方便去调用用户自定义的View,所以我们这里应该设置一个接口,便于用户去继承,然后去设置自己的View
public abstract class BannerAdapter{
/**
* 根据位置获取ViewPager里面的子View
* @param position
* @return
*/
public abstract View getView(int position);
}
用户要使用的时候,只需要继承BannerAdapter,然后实现getView()方法中的逻辑
mBannerVp.setAdapter(new BannerAdapter() {
@Override
public View getView(int position) {
ImageView imageView = new ImageView(BannerActivity.this);
String imagePath = mData.get(0).getCoverMiddle();
Glide.with(BannerActivity.this).load(imagePath)
.placeholder(R.drawable.ic_launcher_foreground)//加载占位图(默认图片)
.into(imageView);
return imageView;
}
});
而这里调用的Adapter就是我们在BannerViewPager中设置的setaAdapter方法,它里面调用的setAdapter方法就是其父类ViewPager中的setAdapter方法
public void setAdapter( BannerAdapter adapter) {
this.mAdapter = adapter;
//设置父类 ViewPager的adapter
setAdapter(new BannerPagerAdapter());
//这里设置Adapter之后,会不断的循环调用instantiateItem()方法,去增加ItemView
}
4. 实现自动轮播
实现方式
- Timer类写一个定时器
- Handler发送消息
- Thread().start()开一个子线程
我们这里采用Handler的方式来实现,不过得注意Handler的内存泄漏问题(Activity的生命周期没有Handler的生命周期长)
//2.实现自动轮播 -- 发送消息的messageWhat
private final int SCROLL_MSG = 0X0011;
//2.实现自动轮播 -- 页面切换间隔时间(默认值)
private int mCutDownTime = 2500;
//这种方式待容易造成内存泄漏
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
//每个多少秒X秒切换到下一页
//切换到下一页
setCurrentItem(getCurrentItem() + 1);
//不断循环执行
startRoll();
}
};
这里创建的Handler一直在执行,没有被销毁,这样下去会造成内存泄漏的情况,因为Handler一直在执行,Activity就不会调用onDestory()方法去销毁自己
解决Handler内存泄漏方法:
4.1. 在Activity退出当前页面的时候,把mHandler停止并销毁
**
* 销毁Handler停止发送 解决内存泄漏
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeMessages(SCROLL_MSG);
mHandler = null;
}
4.2. 使用静态内部类:
private InnerHandler mHandler = new InnerHandler();
private static class InnerHandler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
//doSomething...
}
}
4.3. 使用弱引用解决静态内部类访问外部类
private InnerHandler mHandler = new InnerHandler(BannerViewPager.this){};
private static class InnerHandler extends Handler{
private WeakReference mWeakReference;
public InnerHandler(BannerViewPager bannerViewPager){
mWeakReference = new WeakReference<>(bannerViewPager);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (mWeakReference.get() != null){
//doSomething...
}
}
}
但就算如此,在退出MainActivity后,Looper线程的消息队列中还是可能会有待处理的消息,因此建议在Activity销毁时,移除消息队列中的消息。
4.4. 最后一道关卡:
其实也就是我们第一个方法的用法,在当前的View退出Window时,把Handler的任务清空,并销毁Handler,这样就能保证不会内存泄漏了
/**
* 销毁Handler停止发送 解决内存泄漏
*/
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeMessages(SCROLL_MSG);
mHandler = null;
}
继续回到我们的Handler的使用
这里创建并实例化了Handler,然后我们要开始使用他,并提供方法,让用户手动去开启自动轮播
/**
* 实现自动轮播
*/
public void startRoll(){
//清除消息,防止被多次调用时,间隔时间就没有2500了
mHandler.removeMessages(SCROLL_MSG);
//参数: 消息,延迟时间
//需求:让用户自定义,但也要有个默认值 2500
mHandler.sendEmptyMessageDelayed(SCROLL_MSG,mCutDownTime);
}
5. 设置滚动动画的速度
5.1. 分析源码,寻找办法
根据我们之前的源码分析,在调用ViewPager的setCurrentItem的时候,这个方法的最后会调用
void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) {
//创建和销毁itemView
populate(item);
scrollToItem(item, smoothScroll, velocity, dispatchSelected);
}
private void scrollToItem(int item, boolean smoothScroll, int velocity,
boolean dispatchSelected) {
if (smoothScroll) {
smoothScrollTo(destX, 0, velocity);
} else {
completeScroll(false);
scrollTo(destX, 0);
}
}
private Scroller mScroller;
private static final int MAX_SETTLE_DURATION = 600; // ms
void smoothScrollTo(int x, int y, int velocity) {
int duration;
//默认600ms
duration = Math.min(duration, MAX_SETTLE_DURATION);
mScroller.startScroll(sx, sy, dx, dy, duration);
}
这个滚动最终会调到Scroller的startScroll()方法上来,但是通过源码的查看,我们发现只有改变Scroller这个对象才能改变他切换的时间
所以现在我们改变ViewPager切换速率的方式有两种
- duration 持续时间,但他是局部变量
- 改变mScroller,但是他是private属性,所以通过反射去设置
duration是局部变量,我们无从下手,但是反射改变mScroller还是可行的
5.2. 自定义Scroller去替换ViewPager中的mScroller
我们首先要创建一个Scroller
/**
* 改变ViewPager切换的速率
*/
public class BannerScroller extends Scroller {
//动画持续时间
private int mScrollerDuration = 950;
/**
* 设置切换页面持续的时间
* @param scrollerDuration
*/
public void setScrollerDuration(int scrollerDuration) {
mScrollerDuration = scrollerDuration;
}
public BannerScroller(Context context) {
super(context);
}
public BannerScroller(Context context, Interpolator interpolator) {
super(context, interpolator);
}
public BannerScroller(Context context, Interpolator interpolator, boolean flywheel) {
super(context, interpolator, flywheel);
}
@Override
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
super.startScroll(startX, startY, dx, dy, mScrollerDuration);
}
}
5.3. 在自定义ViewPager中使用反射,改变mScroller的值
然后我们在自定义的ViewPager的构造函数中,去使用反射
public BannerViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//改变ViewPager切换的速率,两种方式
//1. duration 持续时间,但他是局部变量
//2. 改变mScroller,但是这个属性是private的,所以通过反射去设置
try {
//获取属性
Field field = ViewPager.class.getDeclaredField("mScroller");
mScroller = new BannerScroller(context);
//设置为强制改变private,不然可能提示我们不能修改私有属性
field.setAccessible(true);
//设置参数 第一个object当前属性在哪个类 第二个参数代表要设置的值
field.set(this, mScroller);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
6. 自定义BannerView: 封装轮播图和指示器
由于我们一直使用的ViewPager做轮播图,现在如果想要给轮播图加上指示器(文字+点)的话,只能对ViewPager和指示器做再一次的封装,把他们封装到一个BannerView里面,这样我们使用的时候,才能做到简单方便
6.1. 封装布局
先用一个布局把ViewPager和存放点的ViewGroup封装到一个布局中
6.2. 创建BannerView类
布局建好之后,我们要把布局和当前的这个类绑定起来
//把布局加载到View这个里面
inflate(context, R.layout.ui_banner_layout,this);
然后要把ViewPager的Adapter换到这里来,因为我们现在对外用的是BannerView了,而不是ViewPager了
public void setAdapter(BannerAdapter adapter) {
mAdapter = adapter;
mBannerVp.setAdapter(adapter);
//初始化点的指示器
initDotIndicator();
}
当然,设置了Adapter,也就要开始设置ViewPager的滚动速度了,这个在上面已经分析过了
public void startRoll() {
mBannerVp.startRoll();
}
其实目前为止,我们都是在对ViewPager进行包装,把ViewPager的属性,通过BannerView抛给外界使用
下面就是轮播图的圆点指示器了
6.3. 指示器+ 数据描述
这个圆点指示器我们可以直接用ImageView去反映指示器的状态,也可以自定义一个View类,通过设置View的Drawable来反映指示器的状态,这里采用第二种
import androidx.annotation.Nullable;
public class DotIndicatorView extends View {
private Drawable mDrawable;
public DotIndicatorView(Context context) {
this(context,null);
}
public DotIndicatorView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs,0);
}
public DotIndicatorView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
if (mDrawable != null){
mDrawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight());
mDrawable.draw(canvas);
}
}
/**
* 设置Drawable
* @param drawable
*/
public void setDrawable(Drawable drawable) {
this.mDrawable = drawable;
//重新绘制View
invalidate();
}
}
创建好类之后,我们开始初始化指示器的圆点,初始化:位置、大小、间距、状态、监听等等,我们设置监听的目的,是为了在ViewPager滑动的时候,让我们根据当前的位置去获取数据
private void initDotIndicator() {
int count = mAdapter.getCount();
//让点的位置在轮播图的右边
mDotContainer.setGravity(Gravity.END);
for (int i = 0; i < count; i++) {
//不断的往点的指示器添加圆点
DotIndicatorView indicatorView = new DotIndicatorView(mContext);
//设置大小
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dip2px(8), dip2px(8));
indicatorView.setLayoutParams(params);
//设置左右间距
params.leftMargin = params.rightMargin = dip2px(2);
if (i == 0){
//选中位置
indicatorView.setDrawable(mIndicatorFocusDrawable);
}else{
//未选中的
indicatorView.setDrawable(mIndicatorNormalDrawable);
}
mDotContainer.addView(indicatorView);
}
//监听
mBannerVp.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
//监听当前选中的位置
pageSelect(position);
}
});
}
private int dip2px(int dip) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dip,getResources().getDisplayMetrics());
}
/**
* 页面切换的回调
* @param position
*/
private void pageSelect(int position) {
//把之前选中状态的点改为正常
DotIndicatorView oldIndicatorView = (DotIndicatorView) mDotContainer.getChildAt(mCurrentPosition);
oldIndicatorView.setDrawable(mIndicatorNormalDrawable);
//更新当前的位置,先更新,再设置
mCurrentPosition = position%mAdapter.getCount();
//把当前位置的点,改成选中状态 position: 0 --> 2^31
DotIndicatorView currentIndicatorView = (DotIndicatorView) mDotContainer.getChildAt(mCurrentPosition);
currentIndicatorView.setDrawable(mIndicatorFocusDrawable);
//设置广告描述
String bannerDesc = mAdapter.getBannerDesc(mCurrentPosition);
mBannerDescTv.setText(bannerDesc);
}
不过这里有一个小bug,就是我们轮播图刚开始的时候,并没有回调监听事件,所以就不会改变广告的描述,所以,我们要在setAdapter的时候,默认设置第一个数据的广告描述
public void setAdapter(BannerAdapter adapter) {
mAdapter = adapter;
mBannerVp.setAdapter(adapter);
//初始化点的指示器
initDotIndicator();
//初始化广告的描述,默认第一条
String bannerDesc = mAdapter.getBannerDesc(mCurrentPosition);
mBannerDescTv.setText(bannerDesc);
}
6.4. 使用圆点指示器
上面的的指示器是我们直接设置Drawable的的Bounds属性,来确定每个点的绘制区域,然后再在画布上画Drawable。
@Override
protected void onDraw(Canvas canvas) {
if (mDrawable != null){
// mDrawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight());
// mDrawable.draw(canvas);
//从drawable中得到Bitmap
Bitmap bitmap = drawableToBitmap(mDrawable);
//把Bitmap变为圆的
Bitmap circleBitmap = getCircleBitmap(bitmap);
//把圆形的bitmap绘制到画布上
canvas.drawBitmap(circleBitmap,0,0,null);
}
}
所以我们要画圆形的指示器的话,就得改变DotIndicatorView的绘制方式。我们先要把Drawable变成矩形的Bitmap,然后再把矩形的Bitmap变为圆的,最后再把圆的Bitmap画在画布上
-
把Drawable变成Bitmap
private Bitmap drawableToBitmap(Drawable drawable) { //如果是BitmapDrawable类型 if (drawable instanceof BitmapDrawable){ return ((BitmapDrawable)drawable).getBitmap(); } //如果是其他类型 ColorDrawable //创建一个什么也没有的bitmap Bitmap outBitmap = Bitmap.createBitmap(getMeasuredWidth(), getHeight(), Bitmap.Config.ARGB_8888); //创建一个画布 Canvas canvas = new Canvas(outBitmap); //把Drawable画到Bitmap上 drawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight()); drawable.draw(canvas); return outBitmap; }
这里先判断传过来的Drawable是不是BitmapDrawable的子类,如果是就直接返回Bitmap了,没必要转换了,如果不是他的子类,就需要先创建一个Bitmap对象,然后再创建一个画布(把Bitmap和画布绑定起来),最后再把Drawable绘制到Bitmap上,由于之前画布和Bitmap已经绑定,所以此时Bitmap上已经有绘制的Drawable,也就达到了我们把Drawable转换成Bitmap的目的
-
把Bitmap变为圆的
private Bitmap getCircleBitmap(Bitmap bitmap) { //创建一个圆形的Bitmap Bitmap circleBitmap = Bitmap.createBitmap(getMeasuredWidth(),getMeasuredHeight(),Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(circleBitmap); Paint paint = new Paint(); //抗锯齿 paint.setAntiAlias(true); paint.setFilterBitmap(true); //仿抖动 paint.setDither(true); //在画布上面画个圆 canvas.drawCircle(getMeasuredWidth()/2,getMeasuredHeight()/2,getMeasuredWidth()/2,paint); //设置model,取圆和bitmap矩阵的交集的模式 ---srcIn paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); //再把原来的bitmap绘制到新的圆上面 canvas.drawBitmap(bitmap,0,0,paint); return circleBitmap; }
他的思路也是先创建一个新的Bitmap,然后再创建一个Paint对象,再用Paint对象在Canvas上面绘制出一个圆,然后注意,我们需要改变Paint的模式,因为接下来我们要绘制一个矩形(Drawable变成Bitmap时生成的Bitmap矩形),这个时候再用默认的
SRC_OVER
模式,会让矩形部分把圆的部分遮住
但这不是我们想要的效果,我们想要两个的交集部分显示出来
这样,所以我们要设置他的模式为SRC_IN
,让矩形和原型的交集部分 = 圆给显示出来,设置完Paint的模式后,我们把之前的矩形Bitmap绘制到canvas上面去,此时两个Bitmap的区域会重叠,Paint绘制的时候,绘制的是两个的交集部分
- 最后,把圆形Bitmap绘制到onDraw()方法的Canvas上面去。这样我们的圆形指示器就完工了
到了这里,其实我们的功能做的差不多了,效果看上去也很ok,但是要考虑一下他的扩展性问题了,因为不可能每次使用都来看一遍源码,然后开始改源码,我们得提供一个高效的办法。所以就有了我们的自定义属性
7. 自定义属性
我们要先确定自己要定义的属性有哪些
- 点的颜色(选中和未选中)
- 点的大小
- 点的间距
- 点的位置
- 底部颜色
7.1. 定义属性
创建attr.xml文件
7.2. 在布局中使用
7.3. 获取自定义属性
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
//把布局加载到View这个里面
inflate(context, R.layout.ui_banner_layout,this);
//初始化View
initView();
//初始化自定义属性
initAttribute(attrs);
}
private void initAttribute(AttributeSet attrs) {
TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.BannerView);
//点的位置
mDotGravity = typedArray.getInt(R.styleable.BannerView_dotGravity, mDotGravity);
//点的颜色
mIndicatorFocusDrawable = typedArray.getDrawable(R.styleable.BannerView_dotIndicatorFocus);
if (mIndicatorFocusDrawable == null){
//如果在布局文件中没有配置点的颜色,有一个默认值
mIndicatorFocusDrawable = new ColorDrawable(Color.RED);
}
mIndicatorNormalDrawable = typedArray.getDrawable(R.styleable.BannerView_dotIndicatorNormal);
if (mIndicatorNormalDrawable == null){
//如果在布局文件中没有配置点的颜色,有一个默认值
mIndicatorNormalDrawable = new ColorDrawable(Color.WHITE);
}
//获取点的大小和距离
mDotSize = (int) typedArray.getDimension(R.styleable.BannerView_dotSize, dip2px(mDotSize));
mDotDistance = typedArray.getDimensionPixelSize(R.styleable.BannerView_dotDistance, dip2px(mDotDistance));
typedArray.recycle();
}
记得把初始化指示器的地方改成我们的自定义属性
//设置大小
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dip2px(mDotSize), dip2px(mDotSize));
indicatorView.setLayoutParams(params);
//设置左右间距
params.leftMargin = params.rightMargin = dip2px(mDotDistance);
8. 自适应高度
在onMeasure中获取到BannerView的宽度,然后再根据宽度*宽高比,就能得到BannerView的高度,这样就达到了自适应的目的
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量,后面才能获取数据
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//防止后面除数为0
if (mWidthProportion == 0 || mHeightProportion == 0){
return;
}
//动态计算宽高,计算高度
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = (int)(width*mHeightProportion/mWidthProportion);
//指定宽高
setMeasuredDimension(width,height);
}
但是这么设置之后,我们的BannerView仍然不可见,那为什么呢?
其实道理很简单,因为我们测量BannerView的时候,虽然设置了他的宽高比,但是这个时候他的数据还没有进来,BannerView的ViewPager没有数据,所以ViewPager的宽度为0,自然,BannerView的宽度、高度也就为0。
所以解决这个办法也变得简单了,我们在有数据进BannerView的时候,去设置他的高度,这样就能达到适配的效果。所在我们在setAdapter里面去动态设置高度
public void setAdapter(BannerAdapter adapter) {
mAdapter = adapter;
mBannerVp.setAdapter(adapter);
//初始化点的指示器
initDotIndicator();
//初始化广告的描述,默认第一条
String bannerDesc = mAdapter.getBannerDesc(mCurrentPosition);
mBannerDescTv.setText(bannerDesc);
//动态指定高度
//防止后面除数为0
if (mWidthProportion == 0 || mHeightProportion == 0){
return;
}
//动态计算宽高,计算高度
int width = getMeasuredWidth();
int height = (int)(width*mHeightProportion/mWidthProportion);
//指定宽高
getLayoutParams().height = height;
}
9. 代码优化
思考:当我们复用这个控件的时候,
- 是不是足够方便
- 可扩展性强不强
- 内存优化
9.1. Handler内存泄漏问题
内存优化,我们在Handler得内存泄漏的时候已经接触过,如果我们退出Activity的时候,不让Handler的Message清除和销毁Handler,那么我们退出Activity后,Handler任然会继续执行
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mHandler.removeMessages(SCROLL_MSG);
mHandler = null;
}
9.2. 界面复用问题
当我们ViewPager里面滚动的时候,每次都会调用instantiateItem()去新建一个View,并通过destroyItem()方法去销毁一个View
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//Adapter设计模式为了完全让用户自定义
// position 的变化 0 -> 2^31 会溢出,所以我们对总数据进行求模运算
View bannerItemView = mAdapter.getView(position%mAdapter.getCount());
//...
return bannerItemView;
}
/**
* 销毁条目回调的方法
*/
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
object = null;
}
这样每次都创建、销毁,我们就要考虑一下View复用的问题了,我们应该像ListView里面那样,缓存View,然后方便复用
所以我们要在BannerViewPager里面去设置一个View作为缓存的View
//复用
private View mConvertView;
然后修改创建和销毁子View的过程
public Object instantiateItem(@NonNull ViewGroup container, int position) {
//Adapter设计模式为了完全让用户自定义
// position 的变化 0 -> 2^31 会溢出,所以我们对总数据进行求模运算
View bannerItemView = mAdapter.getView(position%mAdapter.getCount(),mConvertView);
//...
return bannerItemView;
}
/**
* 销毁条目回调的方法
*/
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
//缓存
mConvertView = (View) object;
}
我们在创建ItemView的时候,把我们缓存的ItenView传出去,方便用户去复用这个View,而销毁的时候,不再把object销毁,而是缓存起来。然后我们在外面调用的时候,就能使用缓存的View了。
mBannerView.setAdapter(new BannerAdapter() {
@Override
public View getView(int position, View convertView) {
ImageView imageView = null;
//缓存复用
if (convertView == null){
imageView = new ImageView(BannerActivity.this);
imageView.setScaleType(ImageView.ScaleType.FIT_XY);
}else{
imageView = (ImageView) convertView;
}
String imagePath = mData.get(position).getCoverMiddle();
Glide.with(BannerActivity.this).load(imagePath)
.placeholder(R.drawable.ic_launcher_foreground)//加载占位图(默认图片)
.into(imageView);
return imageView;
}
//...
});
但是这样还有一个BUG,就是当我们快速滑动ViewPager的时候,会出现一个错误,显示java.lang.IllegalStateException: The specified child already has a parent. You must call removeView() on the child's parent first.
这个,这是因为,我们滑动得太快,导致还没调用销毁ItemView的方法前(销毁方法之后,convertView就不属于任何父View了),就已经把当前在ViewPager中的convertView又拿来赋值了,此时的convertView是ViewPager中的子View,但我们的操作是在让他重新加入一个父View,这个时候两个父View就冲突了
解决办法,我们应该多缓存几个,以便有足够的View去复用,所以我们的把convertView设置成一个List
//复用
private List mConvertView;
然后销毁ItemView的时候,把销毁的object添加到mConvertView里面去
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
mConvertView.add((View) object);
}
而在创建的时候,我们通过一个方法去判断当前的convertView里面的View是否还有未添加到ViewPager中的,如果有就拿去复用,否则给调用的地方返回一个null,让调用的地方去处理
public Object instantiateItem(@NonNull ViewGroup container, int position) {
bannerItemView = mAdapter.getView(position%mAdapter.getCount(),getConvertView());
}
/**
* 获取复用界面
* @return
*/
private View getConvertView() {
for (int i = 0; i < mConvertView.size(); i++) {
//获取没有添加ViewPager里面的
if (mConvertView.get(i).getParent() != null){
return mConvertView.get(i);
}
}
return null;
}
9.3. bitmap内存优化
在我们创建指示器的点的时候,有一个步骤就是把两个Bitmap重叠,然后只显示其交集,然后返回一个Bitmap,那么另一个Bitmap就没使用了,所以我们最后要去把不再使用的Bitmap给回收掉
private Bitmap getCircleBitmap(Bitmap bitmap) {
//创建一个圆形的Bitmap
Bitmap circleBitmap = Bitmap.createBitmap(getMeasuredWidth(),getMeasuredHeight(),Bitmap.Config.ARGB_8888);
//...
//回收Bitmap
bitmap.recycle();
bitmap = null;
return circleBitmap;
}
9.4. 设置监听回调
在BannerViewPager中,定义接口,并把设置的方法抛给外界使用
/**
* 设置监听
*/
public void setBannerItemClickListener(BannerItemClickListener listener){
this.mListener = listener;
}
/**
* 监听回调
*/
public interface BannerItemClickListener{
void onItemClick(int position);
}
然后在BannerViewPager中,创建ItemView的地方给ItemView设置监听
public Object instantiateItem(@NonNull ViewGroup container, final int position) {
//Adapter设计模式为了完全让用户自定义
// position 的变化 0 -> 2^31 会溢出,所以我们对总数据进行求模运算
View bannerItemView;
bannerItemView = mAdapter.getView(position%mAdapter.getCount(),getConvertView());
//设置监听
bannerItemView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mListener.onItemClick(position%mAdapter.getCount());
}
});
//让用户去添加,实现用户的自定义
// 添加ViewPager里面
container.addView(bannerItemView);
return bannerItemView;
}
和之前得BannerViewPager的属性一样,我们得把BannerViewPager的属性通过BannerView抛给外界使用,所以我们还需要在BannerView中设置
/**
* 点击监听
* @param listener
*/
public void setBannerItemClickListener(BannerViewPager.BannerItemClickListener listener){
mBannerVp.setBannerItemClickListener(listener);
}
这样外界就能监听到BannerView中每个ItemView的点击事件了
9.5. 管理Activity的生命周期
我们看一个现象,在我们的BannerView滚动的时候,我们按下Hone键,回到桌面,此时我们的BannerView已经进入后台进程了,但是我们看控制台的打印,发现BannerViewPager还在滚动,这不是我们想要的,所以我们应该让BannerViewPager和Activity的生命周期结合起来
如果是简单的结合,我们可以在Activity中,去设置BannerViewPager的滚动,但是这样太麻烦,每次都需要我们在Activity去设置,所以,我们要是在BannerViewPager里面监听Activity的生命周期,然后去控制BannerViewPager的滚动,这样就会方便很多。
首先创建一个类,让他实现ActivityLifecycleCallbacks接口,这样,我们只需要实例化的时候,直接实例化DefaultActivityLifecycleCallbacks这个类,然后选择性的重写我们想要的方法,不用把ActivityLifecycleCallbacks中的所有方法都实现了
/**
* 默认实现ActivityLifecycle生命周期的回调
*/
public class DefaultActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
}
然后我们创建一个ActivityLifecycleCallbacks对象,选着性的重写DefaultActivityLifecycleCallbacks里面的onActivityResumed()方法和onActivityPaused()方法
//管理Activity的生命周期
Application.ActivityLifecycleCallbacks mActivityLifecycleCallbacks = new DefaultActivityLifecycleCallbacks(){
@Override
public void onActivityResumed(@NonNull Activity activity) {
//注意监听的是不是当前Activity的生命周期,因为我们这里监听的是所有的Activity的生命周期
Log.d(TAG, "onActivityResumed: current activity --> " + activity);
Log.d(TAG, "onActivityResumed: current getContext() --> " + getContext());
if (activity == getContext()){
//开启轮播
mHandler.sendEmptyMessageDelayed(mCutDownTime,SCROLL_MSG);
}
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
if (activity == getContext()){
//停止轮播
mHandler.removeMessages(SCROLL_MSG);
}
}
};
通过这个对象我们可以去监听当前Activity的生命周期。在设置Adapter的方法里,我们注册一下这个Callback
public void setAdapter( BannerAdapter adapter) {
this.mAdapter = adapter;
//设置父类 ViewPager的adapter
//这里设置Adapter之后,会不断的循环调用instantiateItem()方法,去增加ItemView
setAdapter(new BannerPagerAdapter());
//管理Activity的生命周期
//这里的Context就是Activity
((Activity)getContext()).getApplication().registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
}
这样,当我们的BannerViewPager运行在后台的时候,就会停止滚动了。当再次进入BannerViewPager的时候,就会继续滚动
当然,有注册就有注销,在BannerViewPager退出的时候,我们需要将生命周期的监听从BannerViewPager里面注销掉
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
//解除绑定
((Activity)getContext()).unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
mHandler.removeMessages(SCROLL_MSG);
mHandler = null;
}
10. 总结
这个轮播图到这里就做的差不多了。
三、个人仓库的搭建
1. 发布个人开源库教程:
搭建个人开源库教程
2. 开源库依赖
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
dependencies {
implementation 'com.github.LuoSPro:BannerView:1.0.1'
}
3. gitHub地址
gitHub地址
四、问题:
java.lang.NoSuchMethodError: No virtual method unregisterActivityLifecycleCallbacks(Landroid/app/Application$ActivityLifecycleCallbacks;)V in class Landroid/app/Activity; or its super classes (declaration of 'android.app.Activity' appears in /system/framework/framework.jar)
解决:
因为unregisterActivityLifecycleCallbacks()
方法时Application里面的,而我们在注销时,直接使用
mActivity.unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
这种形式调用的,Activity不能去调用这个方法,会抛异常,所以应该
mActivity.getApplication().unregisterActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
应该这样