顯示的效果請(qǐng)往下看,先說一下需求,可以自動(dòng)輪播,按下停止輪播,松手開始輪播,不可見時(shí)停止輪播,自動(dòng)輪播時(shí)帶動(dòng)畫,手動(dòng)滑動(dòng)時(shí)不帶動(dòng)畫,點(diǎn)擊時(shí)要有水波紋效果,下拉刷新回到第一頁,頁面不能卡頓等等。參考了一些網(wǎng)上的想法,結(jié)合自己的認(rèn)知,總結(jié)如下。
自定義BannerView控件
為了以后開發(fā)的方便,這里將Banner封裝成一個(gè)控件來使用,以后就可以直接在布局里引用。
做這種輪播效果一般采用的都是ViewPager,所以這個(gè)控件也是對(duì)ViewPager的封裝,為了解耦,這里沒有把適配器放在BannerView,只是提供了基類適配器和接口,使用很方便。
講一下具體如何實(shí)現(xiàn)封裝:
第一步:自定義控件,實(shí)現(xiàn)構(gòu)造,初始化屬性和視圖。
- 基于需求,先定義了頁面邊距,主頁面占比,縮放比例,輪播時(shí)長(zhǎng),是否動(dòng)畫輪播等等屬性,通過引用attrs的方式,在布局里賦值,并且提供set方法在代碼里設(shè)置。
private void initAttrs(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.BannerView);
pageMargin = (int) a.getDimension(R.styleable.BannerView_bannerPageMargin, pageMargin);
pagePercent = a.getFloat(R.styleable.BannerView_bannerPagePercent, pagePercent);
scaleMin = a.getFloat(R.styleable.BannerView_bannerPageScale, scaleMin);
alphaMin = a.getFloat(R.styleable.BannerView_bannerPageAlpha, alphaMin);
...
a.recycle();
}
- 定義了基本屬性后,將
ViewPager視圖填充到控件容器里,ViewPager布局里需要使用clipChildren屬性來控制系統(tǒng)繪制View的范圍,在ViewPager和它的父容器里都設(shè)置clipChildren=false。這樣指定了主頁面占比后,左右兩邊的Page就會(huì)進(jìn)行繪制。
private void initView() {
mRootView = LayoutInflater.from(getContext()).inflate(R.layout.banner_view, this);
mViewPager = (ViewPager) mRootView.findViewById(R.id.viewPager);
LayoutParams params = (LayoutParams) mViewPager.getLayoutParams();
params.width = (int) (getScreenWidth() * pagePercent);
params.gravity = Gravity.CENTER;
mViewPager.setLayoutParams(params);
mViewPager.setPageMargin(pageMargin);
mViewPager.setPageTransformer(false, new BannerPageTransformer());
mViewPager.setOffscreenPageLimit(5);
// 自動(dòng)輪播任務(wù)
mScrollTask = new AutoScrollTask();
// 如果動(dòng)畫輪播
if (isAnimScroll) {
setAnimationScroll((int) mAnimDuration);
}
}
第二步:滑動(dòng)動(dòng)畫實(shí)現(xiàn)
眾所周知,谷歌已經(jīng)提供了
ViewPager的滑動(dòng)動(dòng)畫設(shè)置接口setPageTransformer(),并且他自己也實(shí)現(xiàn)了三種基本的滑動(dòng)滑動(dòng)。需要自定義動(dòng)畫時(shí)只需實(shí)現(xiàn)ViewPager.PageTransformer接口,并實(shí)現(xiàn)transformPage()方法。設(shè)計(jì)稿中的動(dòng)畫要求是,滑動(dòng)時(shí)左側(cè)縮小,主頁隨著滑動(dòng)百分比縮小,右側(cè)隨著滑動(dòng)百分比放大。
transformPage方法中提供了主頁所在的position=0.0,左側(cè)位置百分比position<0,右側(cè)位置百分比position>0,通過這個(gè)position可以動(dòng)態(tài)計(jì)算出各個(gè)頁面的縮放比。
public void transformPage(View page, float position) {
// 不同位置的縮放和透明度
float scale = (position < 0)
? ((1 - scaleMin) * position + 1)
: ((scaleMin - 1) * position + 1);
float alpha = (position < 0)
? ((1 - alphaMin) * position + 1)
: ((alphaMin - 1) * position + 1);
// 保持左右兩邊的圖片位置中心
if (position < 0) {
ViewCompat.setPivotX(page, page.getWidth());
ViewCompat.setPivotY(page, page.getHeight() / 2);
} else {
ViewCompat.setPivotX(page, 0);
ViewCompat.setPivotY(page, page.getHeight() / 2);
}
Log.d(TAG, "transformPage: scale=" + scale);
ViewCompat.setScaleX(page, scale);
ViewCompat.setScaleY(page, scale);
ViewCompat.setAlpha(page, Math.abs(alpha));
}
第三步:無限自動(dòng)輪播和輪播動(dòng)畫處理
- 無限輪播可用線程池,定時(shí)器,
Handler等等實(shí)現(xiàn),這里采用最簡(jiǎn)單的Handler實(shí)現(xiàn)。首先自定義一個(gè)輪播任務(wù),實(shí)現(xiàn)Runnable接口,在run方法里使用Handler發(fā)送延時(shí)消息來不停輪播,并且提供start方法和stop方法開啟和停止輪播。在初始化視圖完畢時(shí),開啟輪播。
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
// 視圖初始化完畢,開始輪播任務(wù)
if (mScrollTask == null) mScrollTask = new AutoScrollTask();
if (isAutoScroll) startAutoScroll();
}
-
ViewPager滑動(dòng)是沒有延時(shí)的,谷歌也沒有提供具體的接口去實(shí)現(xiàn)延時(shí)滑動(dòng)。所以這里使用反射去重新設(shè)置ViewPager的滑動(dòng)世間。自動(dòng)輪播時(shí),需求動(dòng)畫延時(shí)滑動(dòng),但是手動(dòng)滑動(dòng)時(shí)需要原生的滑動(dòng),所以根據(jù)使用時(shí)間差和滑動(dòng)時(shí)間來控制自動(dòng)和手動(dòng)滑動(dòng)。
public void startScroll(int startX, int startY, int dx,
int dy, int duration) {
// 如果手動(dòng)滾動(dòng),則加速滾動(dòng)
// TODO 使用這種設(shè)置極不穩(wěn)定,需要抽離
if (System.currentTimeMillis() - mRecentTouchTime > mScrollDuration && isAnimScroll) {
// 動(dòng)畫滑動(dòng)
duration = during;
} else {
// 手勢(shì)滾動(dòng)
duration /= 2;
}
super.startScroll(startX, startY, dx, dy, duration);
}
BannerView的基類適配器封裝
-
BannerView封裝的是ViewPager,所以基類適配器BannerBaseAdapter封裝的也是PagerAdapter,由于適配器涉及到數(shù)據(jù)和視圖,所以基類里將所有都封裝好,只留數(shù)據(jù)和視圖留給子類去實(shí)現(xiàn)。除此之外,頁面的點(diǎn)擊,按下和抬起也在適配器實(shí)現(xiàn)并通過接口暴露出來。 - 實(shí)現(xiàn)父類適配后,只需要指定數(shù)據(jù)類型和實(shí)現(xiàn)展示視圖轉(zhuǎn)換數(shù)據(jù)的方法,加載完數(shù)據(jù)之后,通過setData方法來重新更新數(shù)據(jù)即可。
private class BannerAdapter extends BannerBaseAdapter<BannerBean> {
public BannerAdapter(Context context) {
super(context);
}
@Override
protected int getLayoutResID() {
return R.layout.item_banner;
}
@Override
protected void convert(View convertView, BannerBean data) {
setImage(R.id.pageImage, data.imageRes);
setText(R.id.pageText, data.title);
}
}
使用教程
- 拷貝BannerView全路徑到布局里,使用attrs指定屬性
<com.pinger.widget.banner.BannerView
android:id="@+id/bannerView"
android:layout_width="match_parent"
android:layout_height="200dp"
app:bannerPageAlpha="1.0"
app:bannerPageMargin="8dp"
app:bannerPagePercent="0.8"
app:bannerPageScale="0.8"
app:bannerAnimScroll="true"
app:bannerAutoScroll="true"
app:bannerScrollDuration="4000"
app:bannerAnimDuration="1500"/>
- 在代碼中設(shè)置適配器和設(shè)置頁面的觸摸監(jiān)聽,初始化數(shù)據(jù)之后,更新數(shù)據(jù)
final BannerView bannerView = (BannerView) findViewById(R.id.bannerView);
bannerView.setAdapter(mAdapter = new BannerAdapter(this));
initData(); // 初始化數(shù)據(jù)
mAdapter.setData(mDatas);
mAdapter.setOnPageTouchListener(...);