實現(xiàn)輪轉(zhuǎn)廣告帶底部指示的自定義ViewPager控件
有許多博客和開源項目都致力于這項工作,但是他們的工作大都是為了制作類似于啟動頁的效果,ViewPager全屏顯示,或者自己可操作的屬性難以滿足要求,因此我想把ViewPager和底部的指示物封裝在一個自定義的View中,作為一個新的控件在xml中使用,所以自己來實現(xiàn)了一個。
而且,在用自定義視圖封裝ViewPager時,出現(xiàn)了一個問題,就是ViewPager的所有頁不能全部顯示的問題,不知道是因為這個問題太簡單還是什么其它原因,在網(wǎng)上并沒有搜到這個問題的解決方法(事實上連提問的人都沒有……),困擾了我半個多星期,終于解決,這一點在正文里會介紹,先來貼一下效果圖:
下面來介紹我的實現(xiàn)過程:
首先在res/values/目錄下創(chuàng)建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)的高度,也就是示例圖中圓圈所在灰色透明部分的高度,默認(rèn)為40像素;
dotsSpacing定義底部指示物之間的間距,默認(rèn)為0;
dotsFocusImage定義代表當(dāng)前頁的指示物的樣子;
dotsBlurImage定義代表非當(dāng)前頁的指示物的樣子;
android:scaleType定義ViewPager中ImageView的scale類型,如果ViewPager中的View不是ImageView,則此屬性沒有效果,默認(rèn)為ScaleType.FIT_XY;
android:gravity定義底部指示物在父View(即示例灰色透明部分)的gravity屬性;
dotsBackground定義底部指示物的背景顏色或背景圖;
dotsBgAlpha定義底部指示物的背景顏色或背景圖的透明度,取值為0-1,0代表透明;
changeInteval定義ViewPager自動切換的時間間隔,單位為ms,默認(rèn)為1000ms(這個地方實際的間隔比設(shè)置的要大,不知道是什么原因,望高手解答);
下一步,定義PageAdapter,為ViewPager提供內(nèi)容:
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來規(guī)定如果ViewPager是用來顯示ImageView的,ImageView應(yīng)該怎樣呈現(xiàn)在ViewPager當(dāng)中,如果調(diào)用的構(gòu)造函數(shù)不傳ScaleType信息,則默認(rèn)使用ScaleType.CENTER。
根據(jù)官方API描述,需要重寫PageAdapter的getCount,isViewFromObject,instantiateItem和destroyItem這四個方法,在instantiateItem中設(shè)置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(放置指示物),并且,因為要定期輪轉(zhuǎn),還實現(xiàn)了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指示當(dāng)前正在顯示第幾張圖,isContinue表示可不可以自動輪轉(zhuǎn)(當(dāng)手指觸摸時不輪轉(zhuǎn)),在下面的就是雨attrs.xml中定義的屬性相對應(yīng)的值。作為一個能夠在xml布局文件中直接使用的View,必須重寫擁有Context和AttributeSet參數(shù)的構(gòu)造函數(shù):
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();
}
最后調(diào)用的函數(shù)initView,用來初始化ViewPager和LinearLayout這兩個子視圖,同時,如果xml中給指示物設(shè)置了背景,在這里進(jìn)行設(shè)置:
@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);
}
使用這個類時,關(guān)鍵就是創(chuàng)建一個List<View>,并作為參數(shù)傳進(jìn)來供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就是在底部添加多少個小點,默認(rèn)第一個處于被選中狀態(tài),關(guān)鍵是OnPageChangeListener的onPageSelected方法,這個方法在viewPager進(jìn)行切換時調(diào)用,做的工作就是把底部的指示物切換到對應(yīng)的標(biāo)識上,在這個方法的最后,啟動了輪轉(zhuǎn)的線程。
@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);
}
};
在這個線程中,每隔固定秒數(shù),就向Handler隊列中發(fā)送一個消息,內(nèi)容就是要顯示的view項的index,然后再handler中調(diào)用viewPager的setCurrentItem方法進(jìn)行跳轉(zhuǎn)。至此,最核心的類就完成了,但還剩很關(guān)鍵的一個方法,作為一個自定義的View,要重寫父類的onLayout方法來對子元素進(jìn)行布局,就是這一個方法中不當(dāng)?shù)拇a,導(dǎo)致每次只能顯示前兩張圖,因為ViewPager在顯示時,會默認(rèn)初始化當(dāng)前頁和前后頁,對于第一張來說,沒有前一頁,所以初始化了兩張,在ViewPager滑動時,每次都會調(diào)用onLayout方法,而且,changed參數(shù)為false,我已開始只判斷changed為true時才進(jìn)行布局,就造成了上述問題,完整的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中,創(chuàng)建List<View>數(shù)組并設(shè)置數(shù)據(jù):
@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的方法來控制時間間隔,實際值會比設(shè)置的值長,是因為Message在排隊嗎,第二個問題,就是為什么ViewPager滑動時不重新對ViewPager布局,就會不顯示任何圖,這兩個問題還有待大家解答。
相關(guān)文章
Android短信備份及數(shù)據(jù)插入實現(xiàn)代碼解析
這篇文章主要介紹了Android短信備份及數(shù)據(jù)插入實現(xiàn)代碼解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11Android圖片加載框架Coil的詳細(xì)使用總結(jié)
Coil是Android上的一個全新的圖片加載框架,它的全名叫做coroutine image loader,即協(xié)程圖片加載庫,下面這篇文章主要給大家介紹了關(guān)于Android圖片加載框架Coil詳細(xì)使用的相關(guān)資料,需要的朋友可以參考下2022-07-07ReactNative (API)AsyncStorage存儲詳解及實例
這篇文章主要介紹了ReactNative (API)AsyncStorage存儲詳解及實例的相關(guān)資料,需要的朋友可以參考下2016-10-10Android實現(xiàn)雷達(dá)View效果的示例代碼
這篇文章主要介紹了Android實現(xiàn)雷達(dá)View效果,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06Android 數(shù)據(jù)庫SQLite 寫入SD卡的方法
如果手機沒有root,數(shù)據(jù)庫文件是無法查看到的,不方便調(diào)試。最好的辦法是把數(shù)據(jù)庫寫進(jìn)SD卡。通過本文給大家介紹Android 數(shù)據(jù)庫SQLite 寫入SD卡的方法,需要的朋友參考下吧2016-04-04Android Studio打包APK文件具體實現(xiàn)步驟解析
這篇文章主要介紹了Android Studio打包APK文件具體實現(xiàn)步驟解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-11-11Android中l(wèi)istview和imageview實現(xiàn)條目單選效果
這篇文章主要為大家詳細(xì)介紹了Android中l(wèi)istview和imageview實現(xiàn)條目單選效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-02-02