因为之前项目中有用到自动轮播的效果,然后其实这个东西实现起来的思路并不难想。
所以我直接自己写了一个,然后这个是最近有空余的时间(我他喵什么时候不空了,开题报告不晓得磨了几天了T.T)完完整整的封装了一下,然后加了点扩展功能。如果各位大大懒得自己写也可以直接用我的。
下面是突出重点的分割线~
上面是突出重点的分割线~
然后重点就是~
https://github.com/Linyuzai/Demo4Banner
下面先上效果图(应该可以直接想象=。=)
第三张的效果就是我封装轮播的时候突然想到的,其实很多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要求重新加载。
到此,可以说问题基本解决了=。=
完工睡觉,码了我一晚上,想到有遗漏的再补充~