当前位置: 移动技术网 > IT编程>移动开发>Android > 实现轮转广告带底部指示的自定义ViewPager控件

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

2019年07月24日  | 移动技术网IT编程  | 我要评论

食物禁忌,刀片刺网,美食刑事立花

有许多博客和开源项目都致力于这项工作,但是他们的工作大都是为了制作类似于启动页的效果,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布局,就会不显示任何图,这两个问题还有待大家解答。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网