因為之前項目中有用到自動輪播的效果,然后其實這個東西實現(xiàn)起來的思路并不難想。
所以我直接自己寫了一個,然后這個是最近有空余的時間(我他喵什么時候不空了,開題報告不曉得磨了幾天了T.T)完完整整的封裝了一下,然后加了點擴展功能。如果各位大大懶得自己寫也可以直接用我的。
下面是突出重點的分割線~
上面是突出重點的分割線~
然后重點就是~
https://github.com/Linyuzai/Demo4Banner
下面先上效果圖(應(yīng)該可以直接想象=。=)




第三張的效果就是我封裝輪播的時候突然想到的,其實很多APP里都有這種效果(比如淘票票。。。里面選完電影之后,選日期的導(dǎo)航欄就是這效果~)
然后第四張用的控件和第三個是同一種,特殊化之后就可以有這種APP主界面的效果。
接著是正餐。
我先講一下用法吧。
下面是第一個界面的效果的用法,的分割線
<com.linyuzai.banner.Banner
android:id="@+id/banner"
android:layout_width="match_parent"
android:layout_height="200dp"
banner:auto_duration="750"
banner:banner_interval="3000"
banner:manual_duration="250"
banner:stationary="false" />
| Attrs | Introduction |
|---|---|
| auto_duration | 輪播時自動切換頁面滾動時間 默認(rèn)750ms |
| banner_interval | 自動輪播時間間隔 默認(rèn)5000ms |
| manual_duration | 手動切換的頁面滾動時間 默認(rèn)250ms |
| stationary | 設(shè)置為true,則禁止手動切換頁面 默認(rèn)false |
然后是設(shè)置adapter,這里有兩種adapter,BannerAdapter和BannerAdapter2。先別吐槽名字,我當(dāng)時包括現(xiàn)在都是真心覺得在后面加個2比較適合。
下面是<b>adapter1</b>~
mBanner.setAdapter(new BannerAdapter<ViewHolder>() {
@Override
public int getBannerCount() {
return 0;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
return null;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
}
@Override
public boolean isLoop() {
return true;
}
});
恩,是不是很眼熟,我特意連方法名都和RecyclerView的Adapter一毛一樣啊哈哈哈!其實就是View.setTag(ViewHolder)來用的,我就是把它封裝進去了。
最后一個isLoop()返回true表示可以無限循環(huán),從最后一張到第一張或者從第一張到最后一張,默認(rèn)是false。
然后是<b>adapter2</b>
mBanner.setAdapter(new BannerAdapter2<ViewHolder>() {
@Override
public int getBannerCount() {
return 0;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
return null;
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
}
@Override
public boolean isLoop() {
return true;
}
@Override
public boolean isChangeless() {
return false;
}
});
其他都一樣,多了一個isChangeless(),默認(rèn)false,這個方法用于數(shù)據(jù)更新,如果數(shù)據(jù)只是第一次創(chuàng)建的時候獲取,之后不變動,那么該方法返回true可以減少消耗(或者用adapter1也OK),如果有下拉刷新之類的需要調(diào)用adapter.notifyDataSetChanged()那么默認(rèn)的false就OK。
所以說,<b>BannerAdapter不能支持需要數(shù)據(jù)更新的情況,特別是count改變的情況,而BannerAdapter2可以</b>。
在使用BannerAdapter2進行adapter.notifyDataSetChanged()之后,還需要調(diào)用<b>mBanner.updateBannerAfterDataSetChanged();</b>調(diào)用之后頁面返回到第一張。
當(dāng)然上面所說的<b>支持?jǐn)?shù)據(jù)更新是在設(shè)置為可以無限循環(huán)的前提下</b>。
設(shè)置完adapter之后,只要調(diào)用一下其中一個就可以自動輪播了~
public void startAutoScroll(long delay);
public void startAutoScroll();
對于兩種adapter,分別封裝了一個簡化版的adapter
mBanner.setAdapter(new ImageBannerAdapter() {
@Override
public void onImageViewCreated(ImageView view) {
//view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
}
@Override
public void onBindImage(ImageView image, int position) {
}
@Override
public int getBannerCount() {
return 0;
}
@Override
public boolean isLoop() {
return true;
}
});
mBanner.setAdapter(new ImageBannerAdapter2() {
@Override
public void onImageViewCreated(ImageView view) {
//view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
}
@Override
public void onBindImage(ImageView image, int position) {
}
@Override
public int getBannerCount() {
return 0;
}
@Override
public boolean isLoop() {
return true;
}
@Override
public boolean isChangeless() {
return false;
}
});
另外還有一些其他的方法~
public void stopAutoScroll();//停止自動播放
public void bindIndicator(Indicator indicator);//綁定導(dǎo)航欄,之后會講到
public void setOnBannerItemClickListener(OnBannerItemClickListener listener);//item的點擊事件
public void setOnBannerChangeListener(OnBannerChangeListener listener);//就是ViewPager.OnPageChangeListener
簡單來講就是:
1.設(shè)置adapter;
2.調(diào)用startAutoScroll()。
接下來是第二個界面。碼字好他喵累=。=
<com.linyuzai.banner.hint.HintBanner
android:id="@+id/hint_banner"
android:layout_width="match_parent"
android:layout_height="200dp"
hint:hint_auto_duration="750"
hint:hint_banner_interval="3000"
hint:hint_manual_duration="250" />
三個屬性對應(yīng)Banner的三個屬性,只是多了個前綴,沒有stationary屬性。
HintBanner相對于Banner只是多了類似指示器一樣的幾個點點。所以adapter的設(shè)置和Banner完全一樣。
設(shè)置完adapter之后,添加HintView,提供了三種Creator。
mHintBanner.setHintView(new HintViewCreator() {
@Override
public View getHintView(ViewGroup parent) {
return null;
}
@Override
public void onHintActive(View hint) {
//當(dāng)前頁面相對position的HintView的設(shè)置
}
@Override
public void onHintReset(View hint) {
//切換頁面時還原的上一個HintView的設(shè)置
}
@Override
public ViewLocation getViewLocation() {
return null;//返回HintView的整體位置
}
});
mHintBanner.setHintView(new ColorHintViewCreator() {
@Override
public int getHintActiveColor() {
return Color.WHITE;//當(dāng)前頁面HintView的顏色
}
@Override
public int getHintResetColor() {
return Color.BLACK;//還原上一個HintView的顏色
}
@Override
public boolean isRound() {
return true;//是否是圓的,默認(rèn)方的
}
@Override
public int getViewHeight() {
return 5;//高度
}
@Override
public int getViewWidth() {
return 5;//寬度
}
@Override
public ViewLocation getViewLocation() {
ViewLocation location = ViewLocation.getDefaultViewLocation();
location.setMarginBottom(10);
return location;//返回HintView的整體位置
}
@Override
public int getSpacing() {
return 0;//兩個HintView的間距
}
});
mHintBanner.setHintView(new DrawableHintViewCreator() {
@Override
public Drawable getHintActiveDrawable() {
return getResources().getDrawable(R.mipmap.xxx);//當(dāng)前頁面HintView的Drawable
}
@Override
public Drawable getHintResetDrawable() {
return getResources().getDrawable(R.mipmap.xxx);//還原上一個HintView的Drawable
}
@Override
public int getDrawableHeight() {
return 25;//ImageView的高度
}
@Override
public int getDrawableWidth() {
return 25;//ImageView的寬度
}
@Override
public int getSpacing() {
return 0;//兩個HintView的間距
}
@Override
public ImageView.ScaleType getImageScaleType(){
return ImageView.ScaleType.CENTER_INSIDE;//填充方式,默認(rèn)CENTER_INSIDE
}
});
其中ViewLocation有這些方法~
public static ViewLocation getDefaultViewLocation();//獲得默認(rèn)location,水平居中,豎直對齊底部
public void setVerticalGravity(VerticalGravity vertical);//豎直方向,CENTER, TOP, BOTTOM
public void setHorizontalGravity(HorizontalGravity horizontal);//水平方向,CENTER, RIGHT, LEFT
public void setMarginTop(int marginTop);
public void setMarginBottom(int marginBottom);
public void setMarginLeft(int marginLeft);
public void setMarginRight(int marginRight);
簡單來講就是:
1.設(shè)置adapter;
2.設(shè)置HintView;
3.調(diào)用startAutoScroll()。
恩,然后第三張效果圖(我已經(jīng)不想碼字了=。=)
用到的是Banner+Indicator+adapter(Banner兼容FragmentPagerAdapter等ViewPager所有的adapter)
設(shè)置完adapter之后需要用到Indicator
<com.linyuzai.banner.indicator.Indicator
android:id="@+id/indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/white"
indicator:banner_anim="true"
indicator:cursor_anim="true"
indicator:indicator_anim="true" />
| Attrs | Introduction |
|---|---|
| banner_anim | 綁定Banner之后,頁面切換是否有動畫 默認(rèn)true |
| cursor_anim | 設(shè)置Cursor之后,Cursor是否有動畫 默認(rèn)true |
| indicator_anim | Indicator切換是否有動畫 默認(rèn)true |
Cursor就是上面導(dǎo)航欄底部滑來滑去的那東西
先給Indicator設(shè)置adapter
mIndicator.setAdapter(new BaseIndicatorAdapter<ViewHolder>() {
@Override
public int getIndicatorCount() {
return 0;//item數(shù)量
}
@Override
public ViewHolder onCreateIndicatorViewHolder(ViewGroup parent) {
return null;
}
@Override
public void onBindIndicatorViewHolder(ViewHolder holder, int position) {
}
@Override
public boolean isFitScreenWidth() {
return false;//是否和屏幕一樣寬,并且等分item寬度
}
});
mIndicator.setAdapter(new TextIndicatorAdapter() {
@Override
public void onBindText(TextView text, int position) {
//ViewGroup.LayoutParams params = text.getLayoutParams();
//params.width = 100;
//params.height = 50;
//text.setLayoutParams(params);
//text.setText(TITLE[position]);
//text.setTextColor(Color.GRAY);
}
@Override
public int getIndicatorCount() {
return 0;
}
});
Indicator不用一定要和Banner配合使用,也<b>可以單獨使用</b>。
其中<b>isFitScreenWidth()這個方法,默認(rèn)false,設(shè)置為true就是第四個動圖的效果</b>(記得把banner_anim,cursor_anim,indicator_anim都設(shè)置為false,就能夠瞬間切換。將Banner的stationary設(shè)為false,則可以禁止手動切換)。
可以選擇設(shè)置Cursor
mIndicator.setCursor(new SimpleCursorCreator() {
@Override
public float getHeight() {
return 0;//高度
}
@Override
public int getColor() {
return 0;//顏色
}
@Override
public float getScale() {
return 0;//默認(rèn)和item一樣寬,通過scale調(diào)整寬度
}
@Override
public Paint.Cap getStyle() {
return null;//可以圓弧或有角
}
@Override
public ViewLocation getViewLocation() {
return null;//只支持豎直位置設(shè)置,水平方向無效
}
});
最后第二步,給Indicator設(shè)置OnIndicatorChangeCallback
indicator.setOnIndicatorChangeCallback(new OnIndicatorChangeCallback() {
@Override
public boolean interceptBeforeChange(int position) {
return false;//在Indicator切換之前,可加入操作,返回true攔截Indicator,使之不切換
}
@Override
public void onIndicatorRestore(ViewHolder holder) {
//((TextView) holder.itemView).setTextColor(Color.GRAY);
//還原
}
@Override
public void onIndicatorChange(ViewHolder holder) {
//((TextView) holder.itemView).setTextColor(Color.BLUE);
//切換
}
});
最后一步,綁定Banner和Indicator,可以兩個都綁定,也可以只綁定一個,<b>必須先設(shè)置兩者的adapter</b>
mIndicator.bindBanner(mBanner);//點擊Indicator,切換Banner
mBanner.bindIndicator(mIndicator);//切換Banner,切換Indicator
又要簡單的說了:
1.給Banner設(shè)置adapter;
2.給Indicator設(shè)置adapter;
3.給Indicator設(shè)置Cursor(可選);
4.給Indicator設(shè)置OnIndicatorChangeCallback;
5.綁定Banner和Indicator
好了,用法講完了,下面是思路
說第四個效果圖沒講的你肯定沒有好好看(再碼字有點要BOOM的趕腳)
闡明思路的分割線
上面是一條華麗麗,哦不,十分樸素的分割線,下面簡單闡述思路:
1.首先對于無限輪播,BannerAdapter的思路是getCount()返回Integer.MAX_VALUE
2.Indicator的動畫效果,繼承HorizontalScrollView,在onLayout中記錄每個item的位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//記錄所有導(dǎo)航欄item的位置和寬度
for (int i = 0; i < mIndicatorGroup.getChildCount(); i++) {
View child = mIndicatorGroup.getChildAt(i);
mIndicators[i].left = child.getLeft();//記錄left
mIndicators[i].width = child.getWidth();//記錄width
if (DEBUG)
Log.d(TAG, i + "-->left:" + child.getLeft() + ",width:" + child.getWidth());
}
}
切換時,調(diào)用smoothScrollTo
smoothScrollTo(mIndicators[position].left - (mIndicatorWidth - mIndicators[position].width) / 2, 0);
//mIndicatorWidth看Log的輸出,應(yīng)該等同于屏幕寬度
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mIndicatorWidth = w;
if (DEBUG)
Log.d(TAG, "mIndicatorWidth:" + mIndicatorWidth);
}
所以來個人告訴我onSizeChanged里面的是整個View的寬高還是屏幕可見的寬高。
然后記錄每個item的位置是不是也是在onSizeChanged里面比較好~
繼續(xù)。
Cursor的滑動直接用動畫就OK
ObjectAnimator animator = ObjectAnimator.ofFloat(mCursor, "translationX",
mIndicators[mCurrentPosition].left, mIndicators[position].left);
animator.setDuration(200).start();
//記錄上一次的位置,切換之后更新就好了
3.isFitScreenWidth()我是直接得到屏幕寬度,設(shè)置每個item的寬度為:屏幕寬度 / item數(shù)量
還有一種,設(shè)置所有的ViewGroup為match_parent,HorizontalScrollView.setFillViewport(true);設(shè)置每個item的width=0,weight=1。
不曉得哪種方法比較好,設(shè)置了weight我記得也是要layout兩次的吧。
其實我一開始并沒有寫B(tài)annerAdapter2。直到我腦子一拍,忽略了數(shù)據(jù)更新的測試,才用BannerAdapter測試數(shù)據(jù)更新的。
然后一測,恩,item全亂了。
我們用下面這些代碼計算相對的position
//設(shè)置初始位置
int mOffsetPosition = Integer.MAX_VALUE / 2 % ((BannerAdapter) getAdapter()).getBannerCount();
setCurrentPosition(Integer.MAX_VALUE / 2 - mOffsetPosition);
private void setCurrentPosition(int index) {
if (DEBUG)
Log.d(TAG, "setCurrentPosition---->index:" + index);
try {
Field field = ViewPager.class.getDeclaredField("mCurItem");
field.setAccessible(true);
field.set(this, index);
} catch (Exception e) {
Log.w(TAG, "setCurrentPosition is failed", e);
}
}
//獲得相對位置
modifyPosition = position % ((BannerAdapter) getAdapter()).getBannerCount();
假設(shè)現(xiàn)在我們position=7;bannerCount=3
7 % 3 = 1,下一張的position為8 % 3 = 2;
但是現(xiàn)在數(shù)據(jù)更新了bannerCount = 4;
下一張的position變成了8 % 4 = 0;
所以item會亂一下,之后就正常了。
然后我就想,那我把改變前的position先記下來,然后用新的bannerCount定位
private void resetPosition(int position) {
if (isLoop) {
int mOffsetPosition = Integer.MAX_VALUE / 2 % ((BannerAdapter) getAdapter()).getBannerCount();
setCurrentPosition(Integer.MAX_VALUE / 2 - mOffsetPosition + position);
//setCurrentItem(Integer.MAX_VALUE / 2 - mOffsetPosition + position, false);
}
}
相當(dāng)于記錄當(dāng)前偏移量重新定位,理論上確實可行。
但實際上的效果,如果用反射重新定位,自動輪播的時候會倒退。
如果用setCurrentItem()可能是因為Integer.MAX_VALUE太大,導(dǎo)致屏幕卡住,甚至ANR。
沒有試過重新setAdapter(),感覺消耗更大,于是想有沒有其他的方法。
之后就想到另一種,比如有A,B,C三個頁面。
我將它變成C,A,B,C,A這樣,到position=0的C就立刻切換成position=3的C,到position=4的A的時候立刻切換成position=1的A
然后就有了BannerAdapter2(反正我是想不到什么好名字=。=)
上面又是一條分割線,下面是BannerAdapter2的問題。
添加數(shù)據(jù)的時候沒有什么問題,但是減少數(shù)據(jù)的時候就出問題了,item不但亂了,連自動手動切換也有問題。
然后網(wǎng)上查了一下,發(fā)現(xiàn)原因,PagerAdapter里有這樣一個方法:
@Override
public int getItemPosition(Object object) {
return POSITION_UNCHANGED;
}
改成下面這樣
public int getItemPosition(Object object) {
return POSITION_NONE;//POSITION_NONE意思是沒有找到child要求重新加載。
到此,可以說問題基本解決了=。=
完工睡覺,碼了我一晚上,想到有遺漏的再補充~