实现轮转广告带底部指示的自定义ViewPager控件
有许多博客和开源项目都致力于这项工作,但是他们的工作大都是为了制作类似于启动页的效果,viewpager全屏显示,或者自己可操作的属性难以满足要求,因此我想把viewpager和底部的指示物封装在一个自定义的view中,作为一个新的控件在xml中使用,所以自己来实现了一个。
而且,在用自定义视图封装viewpager时,出现了一个问题,就是viewpager的所有页不能全部显示的问题,不知道是因为这个问题太简单还是什么其它原因,在网上并没有搜到这个问题的解决方法(事实上连提问的人都没有……),困扰了我半个多星期,终于解决,这一点在正文里会介绍,先来贴一下效果图:
下面来介绍我的实现过程:
首先在res/values/目录下创建attrs.xml文件,用来定义新view自定义的属性:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="myviewpager">
<attr name="dotsviewheight" format="dimension" />
<attr name="dotsspacing" format="dimension" />
<attr name="dotsfocusimage" format="reference" />
<attr name="dotsblurimage" format="reference" />
<attr name="android:scaletype" />
<attr name="android:gravity" />
<attr name="dotsbackground" format="reference|color" />
<attr name="dotsbgalpha" format="float" />
<attr name="changeinterval" format="integer" />
</declare-styleable>
</resources>
其中:
dotsviewheight定义底部指示物所在视图(我定义为一个linearlayout)的高度,也就是示例图中圆圈所在灰色透明部分的高度,默认为40像素;
dotsspacing定义底部指示物之间的间距,默认为0;
dotsfocusimage定义代表当前页的指示物的样子;
dotsblurimage定义代表非当前页的指示物的样子;
android:scaletype定义viewpager中imageview的scale类型,如果viewpager中的view不是imageview,则此属性没有效果,默认为scaletype.fit_xy;
android:gravity定义底部指示物在父view(即示例灰色透明部分)的gravity属性;
dotsbackground定义底部指示物的背景颜色或背景图;
dotsbgalpha定义底部指示物的背景颜色或背景图的透明度,取值为0-1,0代表透明;
changeinteval定义viewpager自动切换的时间间隔,单位为ms,默认为1000ms(这个地方实际的间隔比设置的要大,不知道是什么原因,望高手解答);
下一步,定义pageadapter,为viewpager提供内容:
public class viewpageradapter extends pageradapter {
private list<view> views = null;
private scaletype scaletype;
public viewpageradapter(list<view> views) {
this(views, scaletype.center);
}
public viewpageradapter(list<view> views, scaletype scaletype) {
super();
this.views = views;
this.scaletype = scaletype;
}
定义一个views来存储要显示的view,然后定义一个scaletype来规定如果viewpager是用来显示imageview的,imageview应该怎样呈现在viewpager当中,如果调用的构造函数不传scaletype信息,则默认使用scaletype.center。
根据官方api描述,需要重写pageadapter的getcount,isviewfromobject,instantiateitem和destroyitem这四个方法,在instantiateitem中设置scaletype,其它几个方法,都是用官方描述的写法,没有做什么新的改动:
@override
public int getcount() {
// todo auto-generated method stub
return views.size();
}
@override
public boolean isviewfromobject(view arg0, object arg1) {
// todo auto-generated method stub
return arg0 == arg1;
}
@override
public object instantiateitem(view container, int position) {
// todo auto-generated method stub
view view = views.get(position);
viewpager viewpager = (viewpager) container;
if (view instanceof imageview){
((imageview) view).setscaletype(scaletype);
}
viewpager.addview(view, 0);
return view;
}
@override
public void destroyitem(view container, int position, object object) {
// todo auto-generated method stub
((viewpager) container).removeview((view) object);
}
下面就是重头戏了,核心类,被封装的底部带指示物的viewpager,基本思路是自定义一个类继承linearlayout,在里面加入两个子视图viewpager和linearlayout(放置指示物),并且,因为要定期轮转,还实现了runnable接口,定义了以下的变量:
public class myviewpager extends linearlayout implements runnable {
private viewpager viewpager;
private linearlayout viewdots;
private list<imageview> dots;
private list<view> views;
private int position = 0;
private boolean iscontinue = true;
private float dotsviewheight;
private float dotsspacing;
private drawable dotsfocusimage;
private drawable dotsblurimage;
private scaletype scaletype;
private int gravity;
private drawable dotsbackground;
private float dotsbgalpha;
private int changeinterval;
viewpager是要显示的viewpager对象,viewdots是放置指示物的子视图,dots是viewdots上的指示物项,views是viewpager项,position指示当前正在显示第几张图,iscontinue表示可不可以自动轮转(当手指触摸时不轮转),在下面的就是雨attrs.xml中定义的属性相对应的值。作为一个能够在xml布局文件中直接使用的view,必须重写拥有context和attributeset参数的构造函数:
public myviewpager(context context, attributeset attrs) {
super(context, attrs);
// todo auto-generated constructor stub
typedarray a = context.obtainstyledattributes(attrs,
r.styleable.myviewpager, 0, 0);
try {
dotsviewheight = a.getdimension(
r.styleable.myviewpager_dotsviewheight, 40);
//这里依次获取所有的属性值,此处省略,可参看最后附上的全部代码
} finally {
a.recycle();
}
initview();
}
最后调用的函数initview,用来初始化viewpager和linearlayout这两个子视图,同时,如果xml中给指示物设置了背景,在这里进行设置:
@suppresslint("newapi")
private void initview() {
// todo auto-generated method stub
viewpager = new viewpager(getcontext());
viewdots = new linearlayout(getcontext());
layoutparams lp = new layoutparams(layoutparams.match_parent,
layoutparams.match_parent);
addview(viewpager, lp);
if (dotsbackground != null) {
dotsbackground.setalpha((int) (dotsbgalpha * 255));
viewdots.setbackground(dotsbackground);
}
viewdots.setgravity(gravity);
addview(viewdots, lp);
}
使用这个类时,关键就是创建一个list<view>,并作为参数传进来供viewpager(pageradapter)使用,对外的接口就是这个setviewpagerviews:
public void setviewpagerviews(list<view> views) {
this.views = views;
adddots(views.size());
viewpager.setadapter(new viewpageradapter(views, scaletype));
viewpager.setonpagechangelistener(new onpagechangelistener() {
@override
public void onpageselected(int index) {
// todo auto-generated method stub
position = index;
switchtodot(index);
}
//override的两个空方法,此处省略
});
viewpager.setontouchlistener(new ontouchlistener() {
@override
public boolean ontouch(view view, motionevent motionevent) {
// todo auto-generated method stub
switch (motionevent.getaction()) {
case motionevent.action_down:
case motionevent.action_move:
iscontinue = false;
break;
case motionevent.action_up:
iscontinue = true;
break;
default:
iscontinue = true;
break;
}
return false;
}
});
new thread(this).start();
}
adddots就是在底部添加多少个小点,默认第一个处于被选中状态,关键是onpagechangelistener的onpageselected方法,这个方法在viewpager进行切换时调用,做的工作就是把底部的指示物切换到对应的标识上,在这个方法的最后,启动了轮转的线程。
@override
public void run() {
// todo auto-generated method stub
while (true) {
if (iscontinue) {
pagehandler.sendemptymessage(position);
position = (position + 1) % views.size();
try {
thread.sleep(changeinterval);
} catch (interruptedexception e) {
// todo auto-generated catch block
e.printstacktrace();
}
}
}
}
handler pagehandler = new handler() {
@override
public void handlemessage(message msg) {
// todo auto-generated method stub
viewpager.setcurrentitem(msg.what);
super.handlemessage(msg);
}
};
在这个线程中,每隔固定秒数,就向handler队列中发送一个消息,内容就是要显示的view项的index,然后再handler中调用viewpager的setcurrentitem方法进行跳转。至此,最核心的类就完成了,但还剩很关键的一个方法,作为一个自定义的view,要重写父类的onlayout方法来对子元素进行布局,就是这一个方法中不当的代码,导致每次只能显示前两张图,因为viewpager在显示时,会默认初始化当前页和前后页,对于第一张来说,没有前一页,所以初始化了两张,在viewpager滑动时,每次都会调用onlayout方法,而且,changed参数为false,我已开始只判断changed为true时才进行布局,就造成了上述问题,完整的onlayout代码如下:
@override
protected void onlayout(boolean changed, int l, int t, int r, int b) {
// todo auto-generated method stub
view child = this.getchildat(0);
child.layout(0, 0, getwidth(), getheight());
if (changed) {
child = this.getchildat(1);
child.measure(r - l, (int) dotsviewheight);
child.layout(0, getheight() - (int) dotsviewheight, getwidth(),
getheight());
}
}
最后,就是如何使用这个类了,首先,在activity的布局文件中声明这个组件:
<relativelayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:daemon="http://schemas.android.com/apk/res/org.daemon.viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#666666" >
<org.daemon.viewpager.myviewpager
android:id="@+id/my_view_pager"
android:layout_width="match_parent"
android:layout_height="200dp"
daemon:dotsviewheight="30dp"
daemon:dotsfocusimage="@drawable/dot_focused"
daemon:dotsblurimage="@drawable/dot_normal"
daemon:dotsspacing="5dp"
daemon:dotsbackground="#999999"
daemon:dotsbgalpha="0.5"
daemon:changeinterval="3000"
android:scaletype="fitxy"
android:gravity="center" />
</relativelayout>
然后,在mainactivity中,创建list<view>数组并设置数据:
@override
protected void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
initviewpager();
}
private void initviewpager() {
views = new arraylist<view>();
imageview image = new imageview(this);
image.setimageresource(r.drawable.demo_scroll_image);
views.add(image);
image = new imageview(this);
image.setimageresource(r.drawable.demo_scroll_image2);
views.add(image);
image = new imageview(this);
image.setimageresource(r.drawable.demo_coupon_image);
views.add(image);
image = new imageview(this);
image.setimageresource(r.drawable.demo_scroll_image2);
views.add(image);
myviewpager pager = (myviewpager) findviewbyid(r.id.my_view_pager);
pager.setviewpagerviews(views);
}
至此,本示例就全部讲解完了,两个问题,一个就是为什么使用thread的方法来控制时间间隔,实际值会比设置的值长,是因为message在排队吗,第二个问题,就是为什么viewpager滑动时不重新对viewpager布局,就会不显示任何图,这两个问题还有待大家解答。