说到轮播图,想必大家都不陌生。常见的APP都会有一个图片轮播的区域。之前使用过轮播图,最近项目又一次用到了,就把原来的代码照搬过来,结果由于数据结构的差异和照搬使有些代码的疏忽,调试了很久才让原本已经OK的轮播图再次运转起来。所以决定将这个轮播图模块化,做成一个可以通用的组件,方便以后使用。
通过总结网络上各位大神的思路,这里本着学习的态度自定义一个可以无限循环轮播,并且支持手势滑动的轮播图控件。
自定义控件###
自定义View的实现方式大概可以分为三种,自绘控件、组合控件、以及继承控件。这里的实现方式是用第二种方式,组合控件。
组合控件,顾名思义,就是利用Android原生控件通过xml文件布局重定义为自己所需要的UI,然后就此布局文件的控件实现自身需要的功能。
- 定义布局文件
carousel_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="@+id/gallery"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:unselectedAlpha="1"></android.support.v4.view.ViewPager>
<LinearLayout
android:id="@+id/CarouselLayoutPage"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:gravity="center"
android:orientation="horizontal"
android:padding="10dip"></LinearLayout>
</FrameLayout>
这里定义一个Viewpager(用于放置图片),并在下方定义一个横向的LinearLayout(用于放置隋图滚动的小圆点)
- 加载布局文件到View
接下来的步骤,就是将这个xml布局文件结合到需要实现的自定义View当中。
一般,我们在实现自定义控件时,都会继承某一个View(比如LinearLayout,Button或者直接就是View及ViewGroup)。
然后,就需要实现其相应的构造方法,构造方法一般会有3个
public BannerView(Context context) {
super(context);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
建议是这三个构造方法都实现一下。原因可以看看这篇文章
为什么要实现全部三个构造方法
加载布局文件到当前自定义view中
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
this.viewPager.addOnPageChangeListener(this);
addView(view);
}
可以在onFinishInflate这个方法中,加载上述布局文件,并添加到当前view当中.这里这个关于加载view的逻辑,放到构造函数中实现也是可以的。至于放在两个地方的区别我们可以从API文档看出
protected void onFinishInflate ()
Added in API level 1
Finalize inflating a view from XML. This is called as the last phase of inflation, after all child views have been added.
Even if the subclass overrides onFinishInflate, they should always be sure to call the super method, so that we get called.
大概意思就是这个方法会在xml文件所有内容“填充”完成后触发。说白了就是,如果在这个方法里实现了xml的加载,那么在Activity中用java代码new出一个当前自定义View对象时,将没有内容(因为new对象的时候,执行了构造方法,而构造方法中没有加载内容)。其实,大部分情况下,自定义的控件,都会按照完全路径放到xml布局文件中中使用(如本文使用的情况),不会说在代码中new一个,所以,这个addview(view)的逻辑在哪里实现,可以根据实际情况决定(当然,这只是我一时的理解)。
- 初始化
接下来,就需要做一些初始化的工作。
首先可以根据,内容可以绘制出轮播图指示器(即随图滑动的小圆点)
carouselLayout.removeAllViews();
if (adapter.isEmpty()) {
return;
}
int count = adapter.getCount();
showCount = adapter.getCount();
//绘制切换小圆点
for (int i = 0; i < count; i++) {
View view = new View(context);
if (currentPosition == i) {
view.setPressed(true);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
view.setBackgroundResource(R.drawable.carousel_layout_dot);
carouselLayout.addView(view);
}
这里看一下这个carousel_layout_dot.xml 布局文件
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="oval">
<solid android:color="#eb6100"></solid>
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@android:color/transparent"></solid>
<stroke android:width="1dp" android:color="#FFF"> </stroke>
</shape>
</item>
</selector>
做过Button点击效果的同学,对这种模式一定很熟悉。通过view的当前状态,设置不同的色值,可以呈现丰富的视觉效果。这里对小圆点也是一样,选中项设置了高亮的颜色。
通过修改这个文件,可以实现自定义小圆点的效果。列如可以将圆点修改为横线,或者将小圆点切换为图片等,这完全可以根据实际需求决定。
这里使用到了ViewPager,那么Adapter是必不可少了了。这里主要需要实现其selected方法
@Override
public void onPageSelected(int position) {
currentPosition = position;
int count = carouselLayout.getChildCount();
for (int i = 0; i < count; i++) {
View view = carouselLayout.getChildAt(i);
if (position % showCount == i) {
view.setSelected(true);
//当前位置的点要绘制的较大一点
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
view.setSelected(false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
}
}
- 轮播实现
其实,轮播的实现,思路很简单,通过一个独立的线程,不断更改当前位置position,然后使用handler在UI线程中通过ViewPager的SetCurrentItem(position)方法即可实现图片轮播效果。
这里有三点需要注意:
1.选择合适的定时器在适当的位置开始定时任务
2.当用户手指滑动时,如何处理独立线程中对当前位置的更改
3.若要实现无限循环滑动时,滑到第一页和最后一页时如何处理
带着这三个问题,可以看一下完整的代码(这部分代码拆开之后叙述起来会有点乱,所以就给出全部代码)
public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {
private Context context;
private static final int MSG = 0X100;
/**
* 轮播图最大数
*/
private int totalCount = Integer.MAX_VALUE;
/**
* 当前banner需要显式的数量
*/
private int showCount;
private int currentPosition = 0;
private ViewPager viewPager;
private LinearLayout carouselLayout;
private Adapter adapter;
/**
* 轮播切换小圆点宽度默认宽度
*/
private static final int DOT_DEFAULT_W = 5;
/**
* 轮播切换小圆点宽度
*/
private int IndicatorDotWidth = DOT_DEFAULT_W;
/**
* 用户是否干预
*/
private boolean isUserTouched = false;
/**
* 默认的轮播时间
*/
private static final int DEFAULT_TIME = 3000;
/**
* 设置轮播时间
*/
private int switchTime = DEFAULT_TIME;
/**
* 轮播图定时器
*/
private Timer mTimer = new Timer();
public BannerView(Context context) {
super(context);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
private void init() {
viewPager.setAdapter(null);
carouselLayout.removeAllViews();
if (adapter.isEmpty()) {
return;
}
int count = adapter.getCount();
showCount = adapter.getCount();
//绘制切换小圆点
for (int i = 0; i < count; i++) {
View view = new View(context);
if (currentPosition == i) {
view.setPressed(true);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
view.setBackgroundResource(R.drawable.carousel_layout_dot);
carouselLayout.addView(view);
}
viewPager.setAdapter(new ViewPagerAdapter());
viewPager.setCurrentItem(0);
this.viewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//有用户滑动事件发生
isUserTouched = true;
break;
case MotionEvent.ACTION_UP:
isUserTouched = false;
break;
}
return false;
}
});
//以指定周期和岩石开启一个定时任务
mTimer.schedule(mTimerTask, switchTime, switchTime);
}
//设置adapter,这个方法需要再使用时设置
public void setAdapter(Adapter adapter) {
this.adapter = adapter;
if (adapter != null) {
init();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
this.viewPager.addOnPageChangeListener(this);
addView(view);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
currentPosition = position;
int count = carouselLayout.getChildCount();
for (int i = 0; i < count; i++) {
View view = carouselLayout.getChildAt(i);
if (position % showCount == i) {
view.setSelected(true);
//当前位置的点要绘制的较大一点,高亮显示
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
view.setSelected(false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
class ViewPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return totalCount;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
position %= showCount;
View view = adapter.getView(position);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
@Override
public void finishUpdate(ViewGroup container) {
super.finishUpdate(container);
int position = viewPager.getCurrentItem();
if (position == 0) {
position = showCount;
viewPager.setCurrentItem(position, false);
} else if (position == totalCount - 1) {
position = showCount - 1;
viewPager.setCurrentItem(position, false);
}
}
}
private TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
//用户滑动时,定时任务不响应
if (!isUserTouched) {
currentPosition = (currentPosition + 1) % totalCount;
handler.sendEmptyMessage(MSG);
}
}
};
public void cancelTimer() {
if (this.mTimer != null) {
this.mTimer.cancel();
}
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG) {
Log.e("Pos", "the position is " + currentPosition);
if (currentPosition == totalCount - 1) {
viewPager.setCurrentItem(showCount - 1, false);
} else {
viewPager.setCurrentItem(currentPosition);
}
}
}
};
/**
*可自定义设置轮播图切换时间,单位毫秒
* @param switchTime millseconds
*/
public void setSwitchTime(int switchTime) {
this.switchTime = switchTime;
}
/**
* @param indicatorDotWidth
*/
public void setIndicatorDotWidth(int indicatorDotWidth) {
IndicatorDotWidth = indicatorDotWidth;
}
public interface Adapter {
boolean isEmpty();
View getView(int position);
int getCount();
}
}
这里将totalCount的值设置为一个很大的值(这个貌似是实现无限轮播的一个取巧的方法,网上大部分实现都是这样),并将这个值作为ViewPager的个数。每次位置更改时,通过取余数,避免了数组越界,同时巧妙的实现了无限循环轮播效果。
- 测试效果
public class BannerViewActivity extends Activity {
private ListView listview;
private List<String> datas;
private List<String> banners;
private View headView;
private BannerView carouselView;
private Context mContext;
private LayoutInflater mInflater;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.carouse_layout);
InitView();
}
private void InitView() {
InitDatas();
mInflater = LayoutInflater.from(this);
headView = mInflater.inflate(R.layout.carouse_layout_header, null);
carouselView = (BannerView) headView.findViewById(R.id.CarouselView);
//这里考虑到不同手机分辨率下的情况
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
carouselView.setLayoutParams(params);
carouselView.setSwitchTime(2000);
carouselView.setAdapter(new MyAdapter());
listview = V.f(this, R.id.list);
listview.addHeaderView(headView);
ArrayAdapter<String> myAdapter = new ArrayAdapter<String>(mContext, android.R.layout.simple_expandable_list_item_1, datas);
listview.setAdapter(myAdapter);
}
/**
* 设定虚拟数据
*/
private void InitDatas() {
datas = new ArrayList<>();
for (int i = 0; i < 20; i++) {
datas.add("the Item is " + i);
}
//图片来自百度
banners = Arrays.asList("http://img1.imgtn.bdimg.com/it/u=2826772326,2794642991&fm=15&gp=0.jpg",
"http://img15.3lian.com/2015/f2/147/d/39.jpg",
"http://img1.3lian.com/2015/a1/107/d/65.jpg",
"http://img1.3lian.com/2015/a1/93/d/225.jpg",
"http://img1.3lian.com/img013/v4/96/d/44.jpg");
}
//这里可以按实际需求做调整,在适当的位置可停止轮播,节省资源
@Override
protected void onPause() {
super.onPause();
if (carouselView != null) {
carouselView.cancelTimer();
}
}
private class MyAdapter implements BannerView.Adapter {
@Override
public boolean isEmpty() {
return banners.size() > 0 ? false : true;
}
@Override
public View getView(final int position) {
View view = mInflater.inflate(R.layout.item, null);
ImageView imageView = (ImageView) view.findViewById(R.id.image); Picasso.with(mContext).load(banners.get(position)).into(imageView);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
T.showShort(mContext,"Now is "+position);
}
});
return view;
}
@Override
public int getCount() {
return banners.size();
}
}
}
carouse_layout_header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.dreamwork.activity.CarouselView.BannerView
android:id="@+id/CarouselView"
android:layout_width="match_parent"
android:layout_height="match_parent"> </com.example.dreamwork.activity.CarouselView.BannerView>
</LinearLayout>
这里BannerViewActivity的布局文件就是一个ListView,这里代码就不在贴出,将BannView自定义控件作为其头部添加到ListView上即可。还可以很灵活的设置轮播图的切换时间,最后设置其Adapter即可。当然,这里 很简单的自定义了一个List存放图片地址,作为测试。实际开发中,可选取接口返回的后台配置的图片地址。
这里在说一下关于这个轮播图高度的设置,Android手机的碎片化,导致现在市场上各种分辨率手机都存在,适配起来就显得特别纠结,这里的处理方法很值得借鉴
//这里考虑到不同手机分辨率下的情况
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
carouselView.setLayoutParams(params);
dip2px,顾名思义就是根据当前手机分辨率将dp转换为px,这类通用的方法,想必大家都很熟悉,这里就是使用这个方法,设定高度为200dp,然后按照不同手机的分辨率再去分配,这中思路不但在这里,很多地方都可以使用。
好了,这样定义一个轮播图控件后,以后使用时只需要在xml文件中定义BannerView,然后根据业务数据设置其Adapter即可,不必在重新复制粘贴一大堆代码;关于这个图片轮播控件的学习就到这里。