Android輪播及擴展

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


上面是突出重點的分割線~
然后重點就是~
https://github.com/Linyuzai/Demo4Banner
下面先上效果圖(應(yīng)該可以直接想象=。=)

banner.gif

hint_banner.gif

indicator_banner.gif

indicator_banner2.gif

第三張的效果就是我封裝輪播的時候突然想到的,其實很多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要求重新加載。

到此,可以說問題基本解決了=。=
完工睡覺,碼了我一晚上,想到有遺漏的再補充~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容