今天闲来无事,梳理一下关于Android中广告banner图的一些技巧。
一般来说,我们的广告Banner要满足这样几个条件:
Banner
单个Item无限循环ok,下面我们就针对这几点一一来进行分析
首先我们要实现一个自动滚动和手动均可滚动的Banner,很显然,我们需要使用到ViewPager
(当然也可以使用RecyclerView
,不过我个人感觉在使用难度上,前者会小一点)。实现无限循环的方式有这样两种:
这种方法是在原数据队头队尾各新增一条数据,新的数据集首位为原数据末尾数据,其末尾为原数据首位数据,中间为原数据,如图所示(图出处不详):
此循环ViewPager
的思路是将第0位item与倒数第二位映射,将最后一位与第一位元素映射,在选中第0位或者最后一位时,设置为相应的位置。这种方法可以实现循环滚动,但是存在问题,重置ViewPager
位置时不能使用切换动画,如果使用了自定义动画,切换会出现问题,因此该方式不作考虑。
另一种是实现思路就是本篇博客采用的方式。对ViewPager
的适配器进行一些改造,直接看代码:
public static class BannerAdapter<T> extends PagerAdapter {
private List data;//数据源
private boolean notify = false;//手动更改了数据源标志位,解决notifyDataSetChanged不起作用的问题
private BaseBannerViewHolder holder;//生成对应View并处理View事件
private boolean isInfinity;//是否允许无限轮播
private OnPageClickListener onPageClickListener;//item点击回调
public BannerAdapter(Context context,boolean isInfinity) {
this.isInfinity = isInfinity;
}
@NonNull
@Override
public Object instantiateItem(@NonNull ViewGroup container, int position) {
View view = getView(container,position);
container.addView(view);
return view;
}
/**
* 生成对应的View
* @param container
* @param position
* @return
*/
private View getView(ViewGroup container, final int position) {
View view = holder.createView();
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(onPageClickListener != null) {
onPageClickListener.onPageClick(position % getRealCount());
}
}
});
holder.bind(position % getRealCount(),data.get(position % getRealCount()));
return view;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
container.removeView((View)object);
}
/**
* 无限模式返回Int最大值,否则返回真实个数
* @return
*/
@Override
public int getCount() {
if(isInfinity) {
return Integer.MAX_VALUE;
} else {
return getRealCount();
}
}
/**
* 返回数据源真实个数
* @return
*/
public int getRealCount() {
return data == null ? 0 : data.size();
}
/**
* 获取初始化第一个item位置
* @return
*/
public int getFirstPosition() {
if(getRealCount() == 0) {
throw new IllegalArgumentException();
}
if(isInfinity) {
//无限模式下从Int最大值中间开始,若从0开始则无法左滑
int center = Integer.MAX_VALUE/2;
center = center - center % getRealCount();
return center;
} else {
return 0;
}
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
/**
* 更新数据源,设置刷新标志
* @param data
*/
public void setData(List data) {
this.data = data;
this.notify = true;
notifyDataSetChanged();
this.notify = false;
}
/**
* 重写该方法以防止notifyDataSetChanged不生效
* @param object
* @return
*/
@Override
public int getItemPosition(@NonNull Object object) {
if(notify) {
return POSITION_NONE;
} else {
return super.getItemPosition(object);
}
}
public void setHolder(BaseBannerViewHolder holder) {
this.holder = holder;
}
public void setOnPageClickListener(OnPageClickListener onPageClickListener) {
this.onPageClickListener = onPageClickListener;
}
}
代码注释非常清楚了,主要逻辑在于getCount
和getFirstPosition
这两个方法,getCount
返回一个极大数可以保证无限滑动的可能性,getFirstPosition
从中间开始取是为了保证刚开始能够左滑。这里我将Adapter
生成View
的逻辑交给了ViewHolder
去完成,对应的代码如下:
/**
* @author 小米Xylitol
* @email [email protected]
* @desc ViewPager视图生成工具
* @date 2018-06-07 16:26
*/
public interface BaseBannerViewHolder<T> {
View createView();
void bind(int position,T bean);
}
public class BannerViewHolder implements BaseBannerViewHolder<String>{
private Context context;
private TextView tvBanner;
private ImageView ivBanner;
public BannerViewHolder(Context context) {
this.context = context;
}
public View createView() {
View view = LayoutInflater.from(context).inflate(R.layout.item_banner,null,false);
tvBanner = view.findViewById(R.id.tv_banner);
ivBanner = view.findViewById(R.id.iv_banner);
return view;
}
public void bind(int position,String s) {
if(position % 3 == 0) {
ivBanner.setBackgroundColor(0xffff0000);
} else if(position % 3 == 1) {
ivBanner.setBackgroundColor(0xff00ff00);
} else {
ivBanner.setBackgroundColor(0xff0000ff);
}
tvBanner.setText(s);
}
}
简单的逻辑,不再多说,哦对了别忘了在给对应View设置数据时使用的position需要转成对应的realPosition,不然会出现数组越界的问题
自动滚动功能,我这里使用了我们的老朋友Handler,使用Handler发出演示意图,并在意图中在此发出一个延时意图,代码如下:
private Handler handler = new Handler();
private Runnable loopRunnable = new Runnable() {
@Override
public void run() {
if(isLoop && !isTouch) {
int currentItem = viewPager.getCurrentItem();
currentItem++;
viewPager.setCurrentItem(currentItem,true);
startScroll();
}
}
};
/**
* 开始自动滚动
*/
public void startScroll() {
handler.removeCallbacksAndMessages(null);
if(isLoop) {
handler.postDelayed(loopRunnable, delayMillis);
}
}
/**
* 暂停自动滚动
*/
public void stopScroll() {
handler.removeCallbacksAndMessages(null);
}
在相应的初始化逻辑中调用startScroll
即可开始自动滚动,若不需要滚动时,调用stopScroll
即可停止自动滚动。
这个功能较为简单,重写OnTouchListener
,判断手势,然后作相应的处理即可
viewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
isTouch = true;
stopScroll();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
isTouch = false;
startScroll();
break;
}
return false;
}
});
可以看到我们定义了一个手指触摸的标志位,在手指按下和滑动时置为true,并停止自动滚动,在手机抬起和取消触摸时将其设置为false,并且开启自动滚动
响应单独的点击事件,我这里写了一个接口来做:
public interface OnPageClickListener {
void onPageClick(int position);
}
在Adapter中对相应的View进行设置:
/**
* 生成对应的View
* @param container
* @param position
* @return
*/
private View getView(ViewGroup container, final int position) {
View view = holder.createView();
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if(onPageClickListener != null) {
onPageClickListener.onPageClick(position % getRealCount());
}
}
});
holder.bind(position % getRealCount(),data.get(position % getRealCount()));
return view;
}
指示条这个东西因为样式有很多差异的,有些Banner喜欢使用数字表示,有些喜欢使用圆点,我这里选择用接口的方式来实现:
/**
* @author 小米Xylitol
* @email [email protected]
* @desc 指示器
* @date 2018-06-07 17:15
*/
public interface PagerIndicator {
void setCurrentItem(int position);
void attachView(ViewGroup viewGroup);
}
可以看到只有两个方法,一个是将indicator
指示器加入到BannerView
,一个是设置当前滑动的位置,在BannerView
中在相应的位置进行调用:
/**
* 配置指示器
*/
private void initIndicator() {
if(indicator != null) {
indicator.attachView(this);
viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
indicator.setCurrentItem(position % adapter.getRealCount());
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
}
}
/**
* 设置指示器
* @param indicator
*/
public void setIndicator(PagerIndicator indicator) {
this.indicator = indicator;
initIndicator();
}
我这里实现了一个圆点的指示器,如下:
/**
* @author 小米Xylitol
* @email [email protected]
* @desc 圆点指示器
* @date 2018-06-07 17:36
*/
public class CirclePageIndicator implements PagerIndicator {
private int size;
private List imageViewList = new ArrayList<>();
private int currentItem = 0;
public CirclePageIndicator(int size) {
this.size = size;
}
@Override
public void setCurrentItem(int position) {
this.currentItem = position;
for(int i = 0;i < imageViewList.size();i++) {
ImageView imageView = imageViewList.get(i);
if(i == currentItem) {
imageView.setImageResource(R.drawable.indicator_selected);
} else {
imageView.setImageResource(R.drawable.indicator_normal);
}
}
}
@Override
public void attachView(ViewGroup viewGroup) {
LinearLayout linearLayout = new LinearLayout(viewGroup.getContext());
linearLayout.setGravity(Gravity.CENTER);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
for(int i = 0;i < size;i++) {
ImageView imageView = new ImageView(viewGroup.getContext());
if(i == currentItem) {
imageView.setImageResource(R.drawable.indicator_selected);
} else {
imageView.setImageResource(R.drawable.indicator_normal);
}
imageViewList.add(imageView);
linearLayout.addView(imageView);
if(i != size - 1) {
ViewGroup.MarginLayoutParams marginLayoutParams = (ViewGroup.MarginLayoutParams) imageView.getLayoutParams();
marginLayoutParams.rightMargin = BannerView.dpToPx(5);
}
}
viewGroup.addView(linearLayout);
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) linearLayout.getLayoutParams();
params.width = RelativeLayout.LayoutParams.MATCH_PARENT;
params.height = BannerView.dpToPx(15);
params.addRule(RelativeLayout.CENTER_HORIZONTAL);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
linearLayout.setBackgroundColor(0x33000000);
}
}
画廊效果说穿了其实就是对ViewGroup中clipChildren属性的使用,该属性的作用是是否不允许子View突破父控件的大小,默认为true,即默认不突破父控件大小,我们这里要实现画廊效果,就需要给BannerView的根View设置其clipChildren属性为false,具体逻辑如下:
/**
* 设置Banner展示左右两侧其余banner
* @param nesting
* @param margin
*/
public void setNesting(boolean nesting,int margin) {
this.isNesting = nesting;
setClipChildren(!isNesting);
if(!isNesting) {
MarginLayoutParams params = (MarginLayoutParams) viewPager.getLayoutParams();
params.leftMargin = dpToPx(margin);
params.rightMargin = dpToPx(margin);
}
}
注意如果在设置了false之后,不对ViewPager
进行margin
的设置,那样还是不会出现预想的效果,因为屏幕宽度已经被占满了
最后,使用起来是这样的:
<com.xylitolz.androidbannerview.BannerView
android:id="@+id/banner_view"
android:layout_width="match_parent"
android:layout_height="120dp"
app:delayMillis="2000"
app:loop="true"
app:infinity="true"
/>
或者在代码中使用:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bannerView = findViewById(R.id.banner_view);
final List data = new ArrayList<>();
data.add("Banner0");
data.add("Banner1");
data.add("Banner2");
bannerView.setData(data,new BannerViewHolder(this));
bannerView.setNesting(true,20);
bannerView.setIndicator(new CirclePageIndicator(data.size()));
bannerView.setOnPageClickListener(new BannerView.OnPageClickListener() {
@Override
public void onPageClick(int position) {
String toast = "this is page " + data.get(position);
Toast.makeText(MainActivity.this,toast,Toast.LENGTH_SHORT).show();
}
});
}
以上这些就是我在做BannerView时遇到的大部分问题了,还有一些遗漏之处,具体看代码吧,哦等下,先上一个效果图:
博客中涉及到的代码地址在github,欢迎fork,与我一起改进这个控件~有任何疑问或者文章中有任何错误,请在评论区留言告诉我,不胜感激~
我的个人博客,欢迎访问,留言~
enjoy~