因?yàn)橹绊?xiàng)目中有用到自動(dòng)輪播的效果,然后其實(shí)這個(gè)東西實(shí)現(xiàn)起來的思路并不難想诡右。
所以我直接自己寫了一個(gè)澡为,然后這個(gè)是最近有空余的時(shí)間(我他喵什么時(shí)候不空了,開題報(bào)告不曉得磨了幾天了T.T)完完整整的封裝了一下夺欲,然后加了點(diǎn)擴(kuò)展功能。如果各位大大懶得自己寫也可以直接用我的今膊。
下面是突出重點(diǎn)的分割線~
上面是突出重點(diǎn)的分割線~
然后重點(diǎn)就是~
https://github.com/Linyuzai/Demo4Banner
下面先上效果圖(應(yīng)該可以直接想象=洁闰。=)
第三張的效果就是我封裝輪播的時(shí)候突然想到的,其實(shí)很多APP里都有這種效果(比如淘票票万细。扑眉。纸泄。里面選完電影之后,選日期的導(dǎo)航欄就是這效果~)
然后第四張用的控件和第三個(gè)是同一種腰素,特殊化之后就可以有這種APP主界面的效果聘裁。
接著是正餐。
我先講一下用法吧弓千。
下面是第一個(gè)界面的效果的用法衡便,的分割線
<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 | 輪播時(shí)自動(dòng)切換頁(yè)面滾動(dòng)時(shí)間 默認(rèn)750ms |
banner_interval | 自動(dòng)輪播時(shí)間間隔 默認(rèn)5000ms |
manual_duration | 手動(dòng)切換的頁(yè)面滾動(dòng)時(shí)間 默認(rèn)250ms |
stationary | 設(shè)置為true,則禁止手動(dòng)切換頁(yè)面 默認(rèn)false |
然后是設(shè)置adapter洋访,這里有兩種adapter镣陕,BannerAdapter和BannerAdapter2。先別吐槽名字姻政,我當(dāng)時(shí)包括現(xiàn)在都是真心覺得在后面加個(gè)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一毛一樣啊哈哈哈鹊碍!其實(shí)就是View.setTag(ViewHolder)來用的,我就是把它封裝進(jìn)去了食绿。
最后一個(gè)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;
}
});
其他都一樣耀销,多了一個(gè)isChangeless(),默認(rèn)false铲汪,這個(gè)方法用于數(shù)據(jù)更新熊尉,如果數(shù)據(jù)只是第一次創(chuàng)建的時(shí)候獲取,之后不變動(dòng)桥状,那么該方法返回true可以減少消耗(或者用adapter1也OK),如果有下拉刷新之類的需要調(diào)用adapter.notifyDataSetChanged()那么默認(rèn)的false就OK硝清。
所以說辅斟,<b>BannerAdapter不能支持需要數(shù)據(jù)更新的情況,特別是count改變的情況芦拿,而BannerAdapter2可以</b>士飒。
在使用BannerAdapter2進(jìn)行adapter.notifyDataSetChanged()之后,還需要調(diào)用<b>mBanner.updateBannerAfterDataSetChanged();</b>調(diào)用之后頁(yè)面返回到第一張蔗崎。
當(dāng)然上面所說的<b>支持?jǐn)?shù)據(jù)更新是在設(shè)置為可以無限循環(huán)的前提下</b>酵幕。
設(shè)置完adapter之后,只要調(diào)用一下其中一個(gè)就可以自動(dòng)輪播了~
public void startAutoScroll(long delay);
public void startAutoScroll();
對(duì)于兩種adapter缓苛,分別封裝了一個(gè)簡(jiǎn)化版的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();//停止自動(dòng)播放
public void bindIndicator(Indicator indicator);//綁定導(dǎo)航欄芳撒,之后會(huì)講到
public void setOnBannerItemClickListener(OnBannerItemClickListener listener);//item的點(diǎn)擊事件
public void setOnBannerChangeListener(OnBannerChangeListener listener);//就是ViewPager.OnPageChangeListener
簡(jiǎn)單來講就是:
1.設(shè)置adapter;
2.調(diào)用startAutoScroll()。
接下來是第二個(gè)界面笔刹。碼字好他喵累=芥备。=
<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" />
三個(gè)屬性對(duì)應(yīng)Banner的三個(gè)屬性,只是多了個(gè)前綴舌菜,沒有stationary屬性萌壳。
HintBanner相對(duì)于Banner只是多了類似指示器一樣的幾個(gè)點(diǎn)點(diǎn)。所以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)前頁(yè)面相對(duì)position的HintView的設(shè)置
}
@Override
public void onHintReset(View hint) {
//切換頁(yè)面時(shí)還原的上一個(gè)HintView的設(shè)置
}
@Override
public ViewLocation getViewLocation() {
return null;//返回HintView的整體位置
}
});
mHintBanner.setHintView(new ColorHintViewCreator() {
@Override
public int getHintActiveColor() {
return Color.WHITE;//當(dāng)前頁(yè)面HintView的顏色
}
@Override
public int getHintResetColor() {
return Color.BLACK;//還原上一個(gè)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;//兩個(gè)HintView的間距
}
});
mHintBanner.setHintView(new DrawableHintViewCreator() {
@Override
public Drawable getHintActiveDrawable() {
return getResources().getDrawable(R.mipmap.xxx);//當(dāng)前頁(yè)面HintView的Drawable
}
@Override
public Drawable getHintResetDrawable() {
return getResources().getDrawable(R.mipmap.xxx);//還原上一個(gè)HintView的Drawable
}
@Override
public int getDrawableHeight() {
return 25;//ImageView的高度
}
@Override
public int getDrawableWidth() {
return 25;//ImageView的寬度
}
@Override
public int getSpacing() {
return 0;//兩個(gè)HintView的間距
}
@Override
public ImageView.ScaleType getImageScaleType(){
return ImageView.ScaleType.CENTER_INSIDE;//填充方式,默認(rèn)CENTER_INSIDE
}
});
其中ViewLocation有這些方法~
public static ViewLocation getDefaultViewLocation();//獲得默認(rèn)location台颠,水平居中褐望,豎直對(duì)齊底部
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);
簡(jiǎn)單來講就是:
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之后坛吁,頁(yè)面切換是否有動(dòng)畫 默認(rèn)true |
cursor_anim | 設(shè)置Cursor之后劳殖,Cursor是否有動(dòng)畫 默認(rèn)true |
indicator_anim | Indicator切換是否有動(dòng)畫 默認(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>可以單獨(dú)使用</b>哆姻。
其中<b>isFitScreenWidth()這個(gè)方法,默認(rèn)false玫膀,設(shè)置為true就是第四個(gè)動(dòng)圖的效果</b>(記得把banner_anim矛缨,cursor_anim,indicator_anim都設(shè)置為false帖旨,就能夠瞬間切換箕昭。將Banner的stationary設(shè)為false,則可以禁止手動(dòng)切換)解阅。
可以選擇設(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肋拔,可以兩個(gè)都綁定,也可以只綁定一個(gè)呀酸,<b>必須先設(shè)置兩者的adapter</b>
mIndicator.bindBanner(mBanner);//點(diǎn)擊Indicator凉蜂,切換Banner
mBanner.bindIndicator(mIndicator);//切換Banner,切換Indicator
又要簡(jiǎn)單的說了:
1.給Banner設(shè)置adapter性誉;
2.給Indicator設(shè)置adapter窿吩;
3.給Indicator設(shè)置Cursor(可選);
4.給Indicator設(shè)置OnIndicatorChangeCallback错览;
5.綁定Banner和Indicator
好了纫雁,用法講完了,下面是思路
說第四個(gè)效果圖沒講的你肯定沒有好好看(再碼字有點(diǎn)要BOOM的趕腳)
闡明思路的分割線
上面是一條華麗麗倾哺,哦不轧邪,十分樸素的分割線,下面簡(jiǎn)單闡述思路:
1.首先對(duì)于無限輪播羞海,BannerAdapter的思路是getCount()返回Integer.MAX_VALUE
2.Indicator的動(dòng)畫效果忌愚,繼承HorizontalScrollView,在onLayout中記錄每個(gè)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());
}
}
切換時(shí)却邓,調(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);
}
所以來個(gè)人告訴我onSizeChanged里面的是整個(gè)View的寬高還是屏幕可見的寬高。
然后記錄每個(gè)item的位置是不是也是在onSizeChanged里面比較好~
繼續(xù)腊徙。
Cursor的滑動(dòng)直接用動(dòng)畫就OK
ObjectAnimator animator = ObjectAnimator.ofFloat(mCursor, "translationX",
mIndicators[mCurrentPosition].left, mIndicators[position].left);
animator.setDuration(200).start();
//記錄上一次的位置简十,切換之后更新就好了
3.isFitScreenWidth()我是直接得到屏幕寬度,設(shè)置每個(gè)item的寬度為:屏幕寬度 / item數(shù)量
還有一種撬腾,設(shè)置所有的ViewGroup為match_parent螟蝙,HorizontalScrollView.setFillViewport(true);設(shè)置每個(gè)item的width=0,weight=1民傻。
不曉得哪種方法比較好胰默,設(shè)置了weight我記得也是要layout兩次的吧。
其實(shí)我一開始并沒有寫B(tài)annerAdapter2饰潜。直到我腦子一拍初坠,忽略了數(shù)據(jù)更新的測(cè)試和簸,才用BannerAdapter測(cè)試數(shù)據(jù)更新的彭雾。
然后一測(cè),恩锁保,item全亂了薯酝。
我們用下面這些代碼計(jì)算相對(duì)的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);
}
}
//獲得相對(duì)位置
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會(huì)亂一下做葵,之后就正常了占哟。
然后我就想,那我把改變前的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)前偏移量重新定位榨乎,理論上確實(shí)可行。
但實(shí)際上的效果瘫筐,如果用反射重新定位蜜暑,自動(dòng)輪播的時(shí)候會(huì)倒退。
如果用setCurrentItem()可能是因?yàn)镮nteger.MAX_VALUE太大策肝,導(dǎo)致屏幕卡住肛捍,甚至ANR。
沒有試過重新setAdapter()之众,感覺消耗更大拙毫,于是想有沒有其他的方法。
之后就想到另一種酝枢,比如有A恬偷,B,C三個(gè)頁(yè)面帘睦。
我將它變成C袍患,A,B竣付,C诡延,A這樣,到position=0的C就立刻切換成position=3的C古胆,到position=4的A的時(shí)候立刻切換成position=1的A
然后就有了BannerAdapter2(反正我是想不到什么好名字=肆良。=)
上面又是一條分割線,下面是BannerAdapter2的問題逸绎。
添加數(shù)據(jù)的時(shí)候沒有什么問題惹恃,但是減少數(shù)據(jù)的時(shí)候就出問題了,item不但亂了棺牧,連自動(dòng)手動(dòng)切換也有問題巫糙。
然后網(wǎng)上查了一下,發(fā)現(xiàn)原因颊乘,PagerAdapter里有這樣一個(gè)方法:
@Override
public int getItemPosition(Object object) {
return POSITION_UNCHANGED;
}
改成下面這樣
public int getItemPosition(Object object) {
return POSITION_NONE;//POSITION_NONE意思是沒有找到child要求重新加載参淹。
到此醉锄,可以說問題基本解決了=。=
完工睡覺浙值,碼了我一晚上恳不,想到有遺漏的再補(bǔ)充~