說到輪播圖,想必大家都不陌生。常見的APP都會(huì)有一個(gè)圖片輪播的區(qū)域。之前使用過輪播圖,最近項(xiàng)目又一次用到了,就把原來的代碼照搬過來,結(jié)果由于數(shù)據(jù)結(jié)構(gòu)的差異和照搬使有些代碼的疏忽,調(diào)試了很久才讓原本已經(jīng)OK的輪播圖再次運(yùn)轉(zhuǎn)起來。所以決定將這個(gè)輪播圖模塊化,做成一個(gè)可以通用的組件,方便以后使用。
通過總結(jié)網(wǎng)絡(luò)上各位大神的思路,這里本著學(xué)習(xí)的態(tài)度自定義一個(gè)可以無限循環(huán)輪播,并且支持手勢(shì)滑動(dòng)的輪播圖控件。
自定義控件###
自定義View的實(shí)現(xiàn)方式大概可以分為三種,自繪控件、組合控件、以及繼承控件。這里的實(shí)現(xiàn)方式是用第二種方式,組合控件。
組合控件,顧名思義,就是利用Android原生控件通過xml文件布局重定義為自己所需要的UI,然后就此布局文件的控件實(shí)現(xiàn)自身需要的功能。
- 定義布局文件
carousel_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.view.ViewPager
android:id="@+id/gallery"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:unselectedAlpha="1"></android.support.v4.view.ViewPager>
<LinearLayout
android:id="@+id/CarouselLayoutPage"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:gravity="center"
android:orientation="horizontal"
android:padding="10dip"></LinearLayout>
</FrameLayout>
這里定義一個(gè)Viewpager(用于放置圖片),并在下方定義一個(gè)橫向的LinearLayout(用于放置隋圖滾動(dòng)的小圓點(diǎn))
- 加載布局文件到View
接下來的步驟,就是將這個(gè)xml布局文件結(jié)合到需要實(shí)現(xiàn)的自定義View當(dāng)中。
一般,我們?cè)趯?shí)現(xiàn)自定義控件時(shí),都會(huì)繼承某一個(gè)View(比如LinearLayout,Button或者直接就是View及ViewGroup)。
然后,就需要實(shí)現(xiàn)其相應(yīng)的構(gòu)造方法,構(gòu)造方法一般會(huì)有3個(gè)
public BannerView(Context context) {
super(context);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
建議是這三個(gè)構(gòu)造方法都實(shí)現(xiàn)一下。原因可以看看這篇文章
為什么要實(shí)現(xiàn)全部三個(gè)構(gòu)造方法
加載布局文件到當(dāng)前自定義view中
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
this.viewPager.addOnPageChangeListener(this);
addView(view);
}
可以在onFinishInflate這個(gè)方法中,加載上述布局文件,并添加到當(dāng)前view當(dāng)中.這里這個(gè)關(guān)于加載view的邏輯,放到構(gòu)造函數(shù)中實(shí)現(xiàn)也是可以的。至于放在兩個(gè)地方的區(qū)別我們可以從API文檔看出
protected void onFinishInflate ()
Added in API level 1
Finalize inflating a view from XML. This is called as the last phase of inflation, after all child views have been added.
Even if the subclass overrides onFinishInflate, they should always be sure to call the super method, so that we get called.
大概意思就是這個(gè)方法會(huì)在xml文件所有內(nèi)容“填充”完成后觸發(fā)。說白了就是,如果在這個(gè)方法里實(shí)現(xiàn)了xml的加載,那么在Activity中用java代碼new出一個(gè)當(dāng)前自定義View對(duì)象時(shí),將沒有內(nèi)容(因?yàn)閚ew對(duì)象的時(shí)候,執(zhí)行了構(gòu)造方法,而構(gòu)造方法中沒有加載內(nèi)容)。其實(shí),大部分情況下,自定義的控件,都會(huì)按照完全路徑放到xml布局文件中中使用(如本文使用的情況),不會(huì)說在代碼中new一個(gè),所以,這個(gè)addview(view)的邏輯在哪里實(shí)現(xiàn),可以根據(jù)實(shí)際情況決定(當(dāng)然,這只是我一時(shí)的理解)。
- 初始化
接下來,就需要做一些初始化的工作。
首先可以根據(jù),內(nèi)容可以繪制出輪播圖指示器(即隨圖滑動(dòng)的小圓點(diǎn))
carouselLayout.removeAllViews();
if (adapter.isEmpty()) {
return;
}
int count = adapter.getCount();
showCount = adapter.getCount();
//繪制切換小圓點(diǎn)
for (int i = 0; i < count; i++) {
View view = new View(context);
if (currentPosition == i) {
view.setPressed(true);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
view.setBackgroundResource(R.drawable.carousel_layout_dot);
carouselLayout.addView(view);
}
這里看一下這個(gè)carousel_layout_dot.xml 布局文件
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="oval">
<solid android:color="#eb6100"></solid>
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@android:color/transparent"></solid>
<stroke android:width="1dp" android:color="#FFF"> </stroke>
</shape>
</item>
</selector>
做過Button點(diǎn)擊效果的同學(xué),對(duì)這種模式一定很熟悉。通過view的當(dāng)前狀態(tài),設(shè)置不同的色值,可以呈現(xiàn)豐富的視覺效果。這里對(duì)小圓點(diǎn)也是一樣,選中項(xiàng)設(shè)置了高亮的顏色。
通過修改這個(gè)文件,可以實(shí)現(xiàn)自定義小圓點(diǎn)的效果。列如可以將圓點(diǎn)修改為橫線,或者將小圓點(diǎn)切換為圖片等,這完全可以根據(jù)實(shí)際需求決定。
這里使用到了ViewPager,那么Adapter是必不可少了了。這里主要需要實(shí)現(xiàn)其selected方法
@Override
public void onPageSelected(int position) {
currentPosition = position;
int count = carouselLayout.getChildCount();
for (int i = 0; i < count; i++) {
View view = carouselLayout.getChildAt(i);
if (position % showCount == i) {
view.setSelected(true);
//當(dāng)前位置的點(diǎn)要繪制的較大一點(diǎn)
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
view.setSelected(false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
}
}
- 輪播實(shí)現(xiàn)
其實(shí),輪播的實(shí)現(xiàn),思路很簡單,通過一個(gè)獨(dú)立的線程,不斷更改當(dāng)前位置position,然后使用handler在UI線程中通過ViewPager的SetCurrentItem(position)方法即可實(shí)現(xiàn)圖片輪播效果。
這里有三點(diǎn)需要注意:
1.選擇合適的定時(shí)器在適當(dāng)?shù)奈恢瞄_始定時(shí)任務(wù)
2.當(dāng)用戶手指滑動(dòng)時(shí),如何處理獨(dú)立線程中對(duì)當(dāng)前位置的更改
3.若要實(shí)現(xiàn)無限循環(huán)滑動(dòng)時(shí),滑到第一頁和最后一頁時(shí)如何處理
帶著這三個(gè)問題,可以看一下完整的代碼(這部分代碼拆開之后敘述起來會(huì)有點(diǎn)亂,所以就給出全部代碼)
public class BannerView extends FrameLayout implements ViewPager.OnPageChangeListener {
private Context context;
private static final int MSG = 0X100;
/**
* 輪播圖最大數(shù)
*/
private int totalCount = Integer.MAX_VALUE;
/**
* 當(dāng)前banner需要顯式的數(shù)量
*/
private int showCount;
private int currentPosition = 0;
private ViewPager viewPager;
private LinearLayout carouselLayout;
private Adapter adapter;
/**
* 輪播切換小圓點(diǎn)寬度默認(rèn)寬度
*/
private static final int DOT_DEFAULT_W = 5;
/**
* 輪播切換小圓點(diǎn)寬度
*/
private int IndicatorDotWidth = DOT_DEFAULT_W;
/**
* 用戶是否干預(yù)
*/
private boolean isUserTouched = false;
/**
* 默認(rèn)的輪播時(shí)間
*/
private static final int DEFAULT_TIME = 3000;
/**
* 設(shè)置輪播時(shí)間
*/
private int switchTime = DEFAULT_TIME;
/**
* 輪播圖定時(shí)器
*/
private Timer mTimer = new Timer();
public BannerView(Context context) {
super(context);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
}
public BannerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.context = context;
}
private void init() {
viewPager.setAdapter(null);
carouselLayout.removeAllViews();
if (adapter.isEmpty()) {
return;
}
int count = adapter.getCount();
showCount = adapter.getCount();
//繪制切換小圓點(diǎn)
for (int i = 0; i < count; i++) {
View view = new View(context);
if (currentPosition == i) {
view.setPressed(true);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
view.setBackgroundResource(R.drawable.carousel_layout_dot);
carouselLayout.addView(view);
}
viewPager.setAdapter(new ViewPagerAdapter());
viewPager.setCurrentItem(0);
this.viewPager.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
//有用戶滑動(dòng)事件發(fā)生
isUserTouched = true;
break;
case MotionEvent.ACTION_UP:
isUserTouched = false;
break;
}
return false;
}
});
//以指定周期和巖石開啟一個(gè)定時(shí)任務(wù)
mTimer.schedule(mTimerTask, switchTime, switchTime);
}
//設(shè)置adapter,這個(gè)方法需要再使用時(shí)設(shè)置
public void setAdapter(Adapter adapter) {
this.adapter = adapter;
if (adapter != null) {
init();
}
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
View view = LayoutInflater.from(context).inflate(R.layout.carousel_layout, null);
this.viewPager = (ViewPager) view.findViewById(R.id.gallery);
this.carouselLayout = (LinearLayout) view.findViewById(R.id.CarouselLayoutPage);
IndicatorDotWidth = ConvertUtils.dip2px(context, IndicatorDotWidth);
this.viewPager.addOnPageChangeListener(this);
addView(view);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
currentPosition = position;
int count = carouselLayout.getChildCount();
for (int i = 0; i < count; i++) {
View view = carouselLayout.getChildAt(i);
if (position % showCount == i) {
view.setSelected(true);
//當(dāng)前位置的點(diǎn)要繪制的較大一點(diǎn),高亮顯示
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
IndicatorDotWidth + ConvertUtils.dip2px(context, 3),
IndicatorDotWidth + ConvertUtils.dip2px(context, 3));
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
} else {
view.setSelected(false);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(IndicatorDotWidth, IndicatorDotWidth);
params.setMargins(IndicatorDotWidth, 0, 0, 0);
view.setLayoutParams(params);
}
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
class ViewPagerAdapter extends PagerAdapter {
@Override
public int getCount() {
return totalCount;
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
position %= showCount;
View view = adapter.getView(position);
container.addView(view);
return view;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
@Override
public void finishUpdate(ViewGroup container) {
super.finishUpdate(container);
int position = viewPager.getCurrentItem();
if (position == 0) {
position = showCount;
viewPager.setCurrentItem(position, false);
} else if (position == totalCount - 1) {
position = showCount - 1;
viewPager.setCurrentItem(position, false);
}
}
}
private TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
//用戶滑動(dòng)時(shí),定時(shí)任務(wù)不響應(yīng)
if (!isUserTouched) {
currentPosition = (currentPosition + 1) % totalCount;
handler.sendEmptyMessage(MSG);
}
}
};
public void cancelTimer() {
if (this.mTimer != null) {
this.mTimer.cancel();
}
}
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (msg.what == MSG) {
Log.e("Pos", "the position is " + currentPosition);
if (currentPosition == totalCount - 1) {
viewPager.setCurrentItem(showCount - 1, false);
} else {
viewPager.setCurrentItem(currentPosition);
}
}
}
};
/**
*可自定義設(shè)置輪播圖切換時(shí)間,單位毫秒
* @param switchTime millseconds
*/
public void setSwitchTime(int switchTime) {
this.switchTime = switchTime;
}
/**
* @param indicatorDotWidth
*/
public void setIndicatorDotWidth(int indicatorDotWidth) {
IndicatorDotWidth = indicatorDotWidth;
}
public interface Adapter {
boolean isEmpty();
View getView(int position);
int getCount();
}
}
這里將totalCount的值設(shè)置為一個(gè)很大的值(這個(gè)貌似是實(shí)現(xiàn)無限輪播的一個(gè)取巧的方法,網(wǎng)上大部分實(shí)現(xiàn)都是這樣),并將這個(gè)值作為ViewPager的個(gè)數(shù)。每次位置更改時(shí),通過取余數(shù),避免了數(shù)組越界,同時(shí)巧妙的實(shí)現(xiàn)了無限循環(huán)輪播效果。
- 測(cè)試效果
public class BannerViewActivity extends Activity {
private ListView listview;
private List<String> datas;
private List<String> banners;
private View headView;
private BannerView carouselView;
private Context mContext;
private LayoutInflater mInflater;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mContext = this;
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.carouse_layout);
InitView();
}
private void InitView() {
InitDatas();
mInflater = LayoutInflater.from(this);
headView = mInflater.inflate(R.layout.carouse_layout_header, null);
carouselView = (BannerView) headView.findViewById(R.id.CarouselView);
//這里考慮到不同手機(jī)分辨率下的情況
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
carouselView.setLayoutParams(params);
carouselView.setSwitchTime(2000);
carouselView.setAdapter(new MyAdapter());
listview = V.f(this, R.id.list);
listview.addHeaderView(headView);
ArrayAdapter<String> myAdapter = new ArrayAdapter<String>(mContext, android.R.layout.simple_expandable_list_item_1, datas);
listview.setAdapter(myAdapter);
}
/**
* 設(shè)定虛擬數(shù)據(jù)
*/
private void InitDatas() {
datas = new ArrayList<>();
for (int i = 0; i < 20; i++) {
datas.add("the Item is " + i);
}
//圖片來自百度
banners = Arrays.asList("http://img1.imgtn.bdimg.com/it/u=2826772326,2794642991&fm=15&gp=0.jpg",
"http://img15.3lian.com/2015/f2/147/d/39.jpg",
"http://img1.3lian.com/2015/a1/107/d/65.jpg",
"http://img1.3lian.com/2015/a1/93/d/225.jpg",
"http://img1.3lian.com/img013/v4/96/d/44.jpg");
}
//這里可以按實(shí)際需求做調(diào)整,在適當(dāng)?shù)奈恢每赏V馆啿?,?jié)省資源
@Override
protected void onPause() {
super.onPause();
if (carouselView != null) {
carouselView.cancelTimer();
}
}
private class MyAdapter implements BannerView.Adapter {
@Override
public boolean isEmpty() {
return banners.size() > 0 ? false : true;
}
@Override
public View getView(final int position) {
View view = mInflater.inflate(R.layout.item, null);
ImageView imageView = (ImageView) view.findViewById(R.id.image); Picasso.with(mContext).load(banners.get(position)).into(imageView);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
T.showShort(mContext,"Now is "+position);
}
});
return view;
}
@Override
public int getCount() {
return banners.size();
}
}
}
carouse_layout_header.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.example.dreamwork.activity.CarouselView.BannerView
android:id="@+id/CarouselView"
android:layout_width="match_parent"
android:layout_height="match_parent"> </com.example.dreamwork.activity.CarouselView.BannerView>
</LinearLayout>
這里BannerViewActivity的布局文件就是一個(gè)ListView,這里代碼就不在貼出,將BannView自定義控件作為其頭部添加到ListView上即可。還可以很靈活的設(shè)置輪播圖的切換時(shí)間,最后設(shè)置其Adapter即可。當(dāng)然,這里 很簡單的自定義了一個(gè)List存放圖片地址,作為測(cè)試。實(shí)際開發(fā)中,可選取接口返回的后臺(tái)配置的圖片地址。
這里在說一下關(guān)于這個(gè)輪播圖高度的設(shè)置,Android手機(jī)的碎片化,導(dǎo)致現(xiàn)在市場(chǎng)上各種分辨率手機(jī)都存在,適配起來就顯得特別糾結(jié),這里的處理方法很值得借鑒
//這里考慮到不同手機(jī)分辨率下的情況
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, ConvertUtils.dip2px(this, 200));
carouselView.setLayoutParams(params);
dip2px,顧名思義就是根據(jù)當(dāng)前手機(jī)分辨率將dp轉(zhuǎn)換為px,這類通用的方法,想必大家都很熟悉,這里就是使用這個(gè)方法,設(shè)定高度為200dp,然后按照不同手機(jī)的分辨率再去分配,這中思路不但在這里,很多地方都可以使用。
好了,這樣定義一個(gè)輪播圖控件后,以后使用時(shí)只需要在xml文件中定義BannerView,然后根據(jù)業(yè)務(wù)數(shù)據(jù)設(shè)置其Adapter即可,不必在重新復(fù)制粘貼一大堆代碼;關(guān)于這個(gè)圖片輪播控件的學(xué)習(xí)就到這里。