在項目中經常需要使用輪轉廣告的效果,在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布局,就會不顯示任何圖,這兩個問題還有待大家解答。