Android轮播及扩展

因为之前项目中有用到自动轮播的效果,然后其实这个东西实现起来的思路并不难想。
所以我直接自己写了一个,然后这个是最近有空余的时间(我他喵什么时候不空了,开题报告不晓得磨了几天了T.T)完完整整的封装了一下,然后加了点扩展功能。如果各位大大懒得自己写也可以直接用我的。
下面是突出重点的分割线~


上面是突出重点的分割线~
然后重点就是~
https://github.com/Linyuzai/Demo4Banner
下面先上效果图(应该可以直接想象=。=)

banner.gif

hint_banner.gif

indicator_banner.gif

indicator_banner2.gif

第三张的效果就是我封装轮播的时候突然想到的,其实很多APP里都有这种效果(比如淘票票。。。里面选完电影之后,选日期的导航栏就是这效果~)
然后第四张用的控件和第三个是同一种,特殊化之后就可以有这种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 轮播时自动切换页面滚动时间
默认750ms
banner_interval 自动轮播时间间隔
默认5000ms
manual_duration 手动切换的页面滚动时间
默认250ms
stationary 设置为true,则禁止手动切换页面
默认false

然后是设置adapter,这里有两种adapter,BannerAdapter和BannerAdapter2。先别吐槽名字,我当时包括现在都是真心觉得在后面加个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表示可以无限循环,从最后一张到第一张或者从第一张到最后一张,默认是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(),默认false,这个方法用于数据更新,如果数据只是第一次创建的时候获取,之后不变动,那么该方法返回true可以减少消耗(或者用adapter1也OK),如果有下拉刷新之类的需要调用adapter.notifyDataSetChanged()那么默认的false就OK。
所以说,<b>BannerAdapter不能支持需要数据更新的情况,特别是count改变的情况,而BannerAdapter2可以</b>。
在使用BannerAdapter2进行adapter.notifyDataSetChanged()之后,还需要调用<b>mBanner.updateBannerAfterDataSetChanged();</b>调用之后页面返回到第一张。
当然上面所说的<b>支持数据更新是在设置为可以无限循环的前提下</b>。
设置完adapter之后,只要调用一下其中一个就可以自动轮播了~

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);//绑定导航栏,之后会讲到

public void setOnBannerItemClickListener(OnBannerItemClickListener listener);//item的点击事件

public void setOnBannerChangeListener(OnBannerChangeListener listener);//就是ViewPager.OnPageChangeListener

简单来讲就是:
1.设置adapter;
2.调用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" />

三个属性对应Banner的三个属性,只是多了个前缀,没有stationary属性。
HintBanner相对于Banner只是多了类似指示器一样的几个点点。所以adapter的设置和Banner完全一样。
设置完adapter之后,添加HintView,提供了三种Creator。

mHintBanner.setHintView(new HintViewCreator() {
            @Override
            public View getHintView(ViewGroup parent) {
                return null;
            }

            @Override
            public void onHintActive(View hint) {
                //当前页面相对position的HintView的设置
            }

            @Override
            public void onHintReset(View hint) {
                //切换页面时还原的上一个HintView的设置
            }

            @Override
            public ViewLocation getViewLocation() {
                return null;//返回HintView的整体位置
            }
        });

mHintBanner.setHintView(new ColorHintViewCreator() {
            @Override
            public int getHintActiveColor() {
                return Color.WHITE;//当前页面HintView的颜色
            }

            @Override
            public int getHintResetColor() {
                return Color.BLACK;//还原上一个HintView的颜色
            }

            @Override
            public boolean isRound() {
                return true;//是否是圆的,默认方的
            }

            @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);//当前页面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;//填充方式,默认CENTER_INSIDE
            }
        });

其中ViewLocation有这些方法~

public static ViewLocation getDefaultViewLocation();//获得默认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.设置adapter;
2.设置HintView;
3.调用startAutoScroll()。
恩,然后第三张效果图(我已经不想码字了=。=)
用到的是Banner+Indicator+adapter(Banner兼容FragmentPagerAdapter等ViewPager所有的adapter)
设置完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之后,页面切换是否有动画
默认true
cursor_anim 设置Cursor之后,Cursor是否有动画
默认true
indicator_anim Indicator切换是否有动画
默认true

Cursor就是上面导航栏底部滑来滑去的那东西
先给Indicator设置adapter

mIndicator.setAdapter(new BaseIndicatorAdapter<ViewHolder>() {
            @Override
            public int getIndicatorCount() {
                return 0;//item数量
            }

            @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()这个方法,默认false,设置为true就是第四个动图的效果</b>(记得把banner_anim,cursor_anim,indicator_anim都设置为false,就能够瞬间切换。将Banner的stationary设为false,则可以禁止手动切换)。
可以选择设置Cursor

mIndicator.setCursor(new SimpleCursorCreator() {
            @Override
            public float getHeight() {
                return 0;//高度
            }

            @Override
            public int getColor() {
                return 0;//颜色
            }

            @Override
            public float getScale() {
                return 0;//默认和item一样宽,通过scale调整宽度
            }

            @Override
            public Paint.Cap getStyle() {
                return null;//可以圆弧或有角
            }

            @Override
            public ViewLocation getViewLocation() {
                return null;//只支持竖直位置设置,水平方向无效
            }
        });

最后第二步,给Indicator设置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>必须先设置两者的adapter</b>

mIndicator.bindBanner(mBanner);//点击Indicator,切换Banner
        
mBanner.bindIndicator(mIndicator);//切换Banner,切换Indicator

又要简单的说了:
1.给Banner设置adapter;
2.给Indicator设置adapter;
3.给Indicator设置Cursor(可选);
4.给Indicator设置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);
    //记录所有导航栏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());
    }
}

切换时,调用smoothScrollTo

smoothScrollTo(mIndicators[position].left - (mIndicatorWidth - mIndicators[position].width) / 2, 0);
//mIndicatorWidth看Log的输出,应该等同于屏幕宽度

@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里面比较好~
继续。
Cursor的滑动直接用动画就OK

ObjectAnimator animator = ObjectAnimator.ofFloat(mCursor, "translationX",
    mIndicators[mCurrentPosition].left, mIndicators[position].left);
animator.setDuration(200).start();
//记录上一次的位置,切换之后更新就好了

3.isFitScreenWidth()我是直接得到屏幕宽度,设置每个item的宽度为:屏幕宽度 / item数量
还有一种,设置所有的ViewGroup为match_parent,HorizontalScrollView.setFillViewport(true);设置每个item的width=0,weight=1。
不晓得哪种方法比较好,设置了weight我记得也是要layout两次的吧。


其实我一开始并没有写BannerAdapter2。直到我脑子一拍,忽略了数据更新的测试,才用BannerAdapter测试数据更新的。
然后一测,恩,item全乱了。
我们用下面这些代码计算相对的position

//设置初始位置
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();

假设现在我们position=7;bannerCount=3
7 % 3 = 1,下一张的position为8 % 3 = 2;
但是现在数据更新了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);
    }
}

相当于记录当前偏移量重新定位,理论上确实可行。
但实际上的效果,如果用反射重新定位,自动轮播的时候会倒退。
如果用setCurrentItem()可能是因为Integer.MAX_VALUE太大,导致屏幕卡住,甚至ANR。
没有试过重新setAdapter(),感觉消耗更大,于是想有没有其他的方法。
之后就想到另一种,比如有A,B,C三个页面。
我将它变成C,A,B,C,A这样,到position=0的C就立刻切换成position=3的C,到position=4的A的时候立刻切换成position=1的A
然后就有了BannerAdapter2(反正我是想不到什么好名字=。=)


上面又是一条分割线,下面是BannerAdapter2的问题。
添加数据的时候没有什么问题,但是减少数据的时候就出问题了,item不但乱了,连自动手动切换也有问题。
然后网上查了一下,发现原因,PagerAdapter里有这样一个方法:

@Override
public int getItemPosition(Object object) {
    return POSITION_UNCHANGED;
}

改成下面这样

public int getItemPosition(Object object) {
    return POSITION_NONE;//POSITION_NONE意思是没有找到child要求重新加载。

到此,可以说问题基本解决了=。=
完工睡觉,码了我一晚上,想到有遗漏的再补充~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容