zoukankan      html  css  js  c++  java
  • 【Android】带底部指示的自定义ViewPager控件

      在项目中经常需要使用轮转广告的效果,在android-v4版本中提供的ViewPager是一个很好的工具,而一般我们使用Viewpager的时候,都会选择在底部有一排指示物指示当前显示的是哪一个page,这么常用的组合如果每次用都重头写当然是一件很麻烦的事情,有许多博客和开源项目都致力于这项工作,但是他们的工作大都是为了制作类似于启动页的效果,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布局,就会不显示任何图,这两个问题还有待大家解答。

      源代码下载

  • 相关阅读:
    使用SharePoint文档库需注意的问题
    Windows 2003 lassess.exe 系统错误
    使用javascript 实现.net 验证控件功能
    NetAdvantage For .NET全新推出 2008 Volume 2 版加强了对web图表的支持
    Aspose.Total 发布Q12010第一季度版
    FastReport VCL4.9发布
    StimulReport.Net报表控件推介
    .NET Reactor超高性价比的混淆器
    TeeChartfor.NET 全面支持VisualStudio2010和.NET Framework4.0 (控件中国网)
    Dundas Dashboard V2.0仪表盘控件的发布
  • 原文地址:https://www.cnblogs.com/smarterplanet/p/3435528.html
Copyright © 2011-2022 走看看