zl程序教程

您现在的位置是:首页 >  其它

当前栏目

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

实现 自定义 控件 广告 底部 Viewpager 轮转 指示
2023-06-13 09:15:12 时间

有许多博客和开源项目都致力于这项工作,但是他们的工作大都是为了制作类似于启动页的效果,ViewPager全屏显示,或者自己可操作的属性难以满足要求,因此我想把ViewPager和底部的指示物封装在一个自定义的View中,作为一个新的控件在xml中使用,所以自己来实现了一个。
而且,在用自定义视图封装ViewPager时,出现了一个问题,就是ViewPager的所有页不能全部显示的问题,不知道是因为这个问题太简单还是什么其它原因,在网上并没有搜到这个问题的解决方法(事实上连提问的人都没有……),困扰了我半个多星期,终于解决,这一点在正文里会介绍,先来贴一下效果图:

下面来介绍我的实现过程:

首先在res/values/目录下创建attrs.xml文件,用来定义新View自定义的属性:

复制代码代码如下:


<?xmlversion="1.0"encoding="utf-8"?>
<resources>
   <declare-styleablename="MyViewPager">
       <attrname="dotsViewHeight"format="dimension"/>
       <attrname="dotsSpacing"format="dimension"/>
       <attrname="dotsFocusImage"format="reference"/>
       <attrname="dotsBlurImage"format="reference"/>
       <attrname="android:scaleType"/>
       <attrname="android:gravity"/>
       <attrname="dotsBackground"format="reference|color"/>
       <attrname="dotsBgAlpha"format="float"/>
       <attrname="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提供内容:

复制代码代码如下:


publicclassViewPagerAdapterextendsPagerAdapter{

   privateList<View>views=null;
   privateScaleTypescaleType;

   publicViewPagerAdapter(List<View>views){
       this(views,ScaleType.CENTER);
   }

   publicViewPagerAdapter(List<View>views,ScaleTypescaleType){
       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
publicintgetCount(){
   //TODOAuto-generatedmethodstub
   returnviews.size();
}

@Override
publicbooleanisViewFromObject(Viewarg0,Objectarg1){
   //TODOAuto-generatedmethodstub
   returnarg0==arg1;
}

@Override
publicObjectinstantiateItem(Viewcontainer,intposition){
   //TODOAuto-generatedmethodstub
   Viewview=views.get(position);
   ViewPagerviewPager=(ViewPager)container;
   if(viewinstanceofImageView){
       ((ImageView)view).setScaleType(scaleType);
   }
   viewPager.addView(view,0);
   returnview;
}

@Override
publicvoiddestroyItem(Viewcontainer,intposition,Objectobject){
   //TODOAuto-generatedmethodstub
   ((ViewPager)container).removeView((View)object);
}

下面就是重头戏了,核心类,被封装的底部带指示物的ViewPager,基本思路是自定义一个类继承LinearLayout,在里面加入两个子视图ViewPager和LinearLayout(放置指示物),并且,因为要定期轮转,还实现了Runnable接口,定义了以下的变量:

复制代码代码如下:
publicclassMyViewPagerextendsLinearLayoutimplementsRunnable{

   privateViewPagerviewPager;
   privateLinearLayoutviewDots;
   privateList<ImageView>dots;
   privateList<View>views;

   privateintposition=0;
   privatebooleanisContinue=true;

   privatefloatdotsViewHeight;
   privatefloatdotsSpacing;
   privateDrawabledotsFocusImage;
   privateDrawabledotsBlurImage;
   privateScaleTypescaleType;
   privateintgravity;
   privateDrawabledotsBackground;
   privatefloatdotsBgAlpha;
   privateintchangeInterval;

viewPager是要显示的ViewPager对象,viewDots是放置指示物的子视图,dots是viewDots上的指示物项,views是ViewPager项,position指示当前正在显示第几张图,isContinue表示可不可以自动轮转(当手指触摸时不轮转),在下面的就是雨attrs.xml中定义的属性相对应的值。作为一个能够在xml布局文件中直接使用的View,必须重写拥有Context和AttributeSet参数的构造函数:

复制代码代码如下:
publicMyViewPager(Contextcontext,AttributeSetattrs){
super(context,attrs);
   //TODOAuto-generatedconstructorstub
   TypedArraya=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")
privatevoidinitView(){
   //TODOAuto-generatedmethodstub
   viewPager=newViewPager(getContext());
   viewDots=newLinearLayout(getContext());

   LayoutParamslp=newLayoutParams(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:

复制代码代码如下:
publicvoidsetViewPagerViews(List<View>views){
   this.views=views;
   addDots(views.size());

   viewPager.setAdapter(newViewPagerAdapter(views,scaleType));

   viewPager.setOnPageChangeListener(newOnPageChangeListener(){
       @Override
       publicvoidonPageSelected(intindex){
           //TODOAuto-generatedmethodstub
           position=index;
           switchToDot(index);
       }
       //override的两个空方法,此处省略
   });

   viewPager.setOnTouchListener(newOnTouchListener(){

       @Override
       publicbooleanonTouch(Viewview,MotionEventmotionevent){
           //TODOAuto-generatedmethodstub
           switch(motionevent.getAction()){
           caseMotionEvent.ACTION_DOWN:
           caseMotionEvent.ACTION_MOVE:
               isContinue=false;
               break;
           caseMotionEvent.ACTION_UP:
               isContinue=true;
               break;
           default:
               isContinue=true;
               break;
           }
           returnfalse;
       }
   });
   newThread(this).start();
}

addDots就是在底部添加多少个小点,默认第一个处于被选中状态,关键是OnPageChangeListener的onPageSelected方法,这个方法在viewPager进行切换时调用,做的工作就是把底部的指示物切换到对应的标识上,在这个方法的最后,启动了轮转的线程。

复制代码代码如下:
@Override
publicvoidrun(){
   //TODOAuto-generatedmethodstub
   while(true){
       if(isContinue){
           pageHandler.sendEmptyMessage(position);
           position=(position+1)%views.size();
           try{
               Thread.sleep(changeInterval);
           }catch(InterruptedExceptione){
               //TODOAuto-generatedcatchblock
               e.printStackTrace();
           }
       }
   }
}

HandlerpageHandler=newHandler(){
   @Override
   publicvoidhandleMessage(Messagemsg){
       //TODOAuto-generatedmethodstub
       viewPager.setCurrentItem(msg.what);
       super.handleMessage(msg);
   }
};

在这个线程中,每隔固定秒数,就向Handler队列中发送一个消息,内容就是要显示的view项的index,然后再handler中调用viewPager的setCurrentItem方法进行跳转。至此,最核心的类就完成了,但还剩很关键的一个方法,作为一个自定义的View,要重写父类的onLayout方法来对子元素进行布局,就是这一个方法中不当的代码,导致每次只能显示前两张图,因为ViewPager在显示时,会默认初始化当前页和前后页,对于第一张来说,没有前一页,所以初始化了两张,在ViewPager滑动时,每次都会调用onLayout方法,而且,changed参数为false,我已开始只判断changed为true时才进行布局,就造成了上述问题,完整的onLayout代码如下:

复制代码代码如下:
@Override
protectedvoidonLayout(booleanchanged,intl,intt,intr,intb){
   //TODOAuto-generatedmethodstub
   Viewchild=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的布局文件中声明这个组件:

复制代码代码如下:
<RelativeLayoutxmlns: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
protectedvoidonCreate(BundlesavedInstanceState){
   super.onCreate(savedInstanceState);
   setContentView(R.layout.activity_main);
   initViewPager();
}

privatevoidinitViewPager(){
   views=newArrayList<View>();

   ImageViewimage=newImageView(this);
   image.setImageResource(R.drawable.demo_scroll_image);
   views.add(image);
   image=newImageView(this);
   image.setImageResource(R.drawable.demo_scroll_image2);
   views.add(image);
   image=newImageView(this);
   image.setImageResource(R.drawable.demo_coupon_image);
   views.add(image);
   image=newImageView(this);
   image.setImageResource(R.drawable.demo_scroll_image2);
   views.add(image);

   MyViewPagerpager=(MyViewPager)findViewById(R.id.my_view_pager);
   pager.setViewPagerViews(views);
}

至此,本示例就全部讲解完了,两个问题,一个就是为什么使用Thread的方法来控制时间间隔,实际值会比设置的值长,是因为Message在排队吗,第二个问题,就是为什么ViewPager滑动时不重新对ViewPager布局,就会不显示任何图,这两个问题还有待大家解答。