欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

实现轮转广告带底部指示的自定义ViewPager控件

程序员文章站 2022-07-11 19:39:43
有许多博客和开源项目都致力于这项工作,但是他们的工作大都是为了制作类似于启动页的效果,viewpager全屏显示,或者自己可操作的属性难以满足要求,因此我想把viewpag...

有许多博客和开源项目都致力于这项工作,但是他们的工作大都是为了制作类似于启动页的效果,viewpager全屏显示,或者自己可操作的属性难以满足要求,因此我想把viewpager和底部的指示物封装在一个自定义的view中,作为一个新的控件在xml中使用,所以自己来实现了一个。
而且,在用自定义视图封装viewpager时,出现了一个问题,就是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布局,就会不显示任何图,这两个问题还有待大家解答。