具有立体感的Material风格循环滚动Banner

摘要

最近在项目中要实现如下效果,在GitHub上搜了一下找到一个很不错的案例,但是没有实现自动循环,所以我决定来在该项目基础上修改一番,下图是我实现的两个效果。

一场APP界面

滑动动画

GitHub项目地址:https://github.com/yarolegovich/DiscreteScrollView

基础使用

gradle.build中引入库

1
compile 'com.yarolegovich:discrete-scrollview:1.2.0'

在需要添加的地方的布局文件中添加

1
2
3
4
5
<com.yarolegovich.discretescrollview.DiscreteScrollView
android:id="@+id/main_scroll_picker"
android:layout_marginTop="5dip"
android:layout_width="match_parent"
android:layout_height="110dip"/>

在代码中进行基本设置

1
2
3
4
5
6
7
8
9
mScrollPick = getView(R.id.main_scroll_picker);
mScrollPick.setOrientation(Orientation.HORIZONTAL);
//mTeamPick.addOnItemChangedListener(this);
mScrollAdapter = new MainScrollPickAdapter(mScrollBanners);
mScrollPick.setAdapter(mScrollAdapter);
mScrollPick.setItemTransitionTimeMillis(500);
mScrollPick.setItemTransformer(new ScaleTransformer.Builder()
.setMinScale(0.9f)
.build());

内容由自定义的适配器决定

1
2
3
4
5
6
7
8
9
10
11
public class MainScrollPickAdapter extends BaseQuickAdapter<ScrollBanner, BaseViewHolder> {

public MainScrollPickAdapter(List<ScrollBanner> data) {
super(R.layout.item_main_scroll_banner, data);
}

@Override
protected void convert(BaseViewHolder helper, ScrollBanner item) {
helper.getView(R.id.item_root).setBackgroundColor(Color.parseColor(item.getBannerBg()));
}
}

初始化一些测试数据

1
2
3
4
5
6
7
mScrollBanners.add(new ScrollBanner("#f24fc9"));
mScrollBanners.add(new ScrollBanner("#bd60f5"));
mScrollBanners.add(new ScrollBanner("#6a63e6"));
mScrollBanners.add(new ScrollBanner("#df3b3b"));
mScrollBanners.add(new ScrollBanner("#f1e599"));

mScrollAdapter.notifyDataSetChanged();

好了,这样就可以用手滑动了,效果是不是很棒呢?此时并没有满足我们最初的需求,我们需要让它自动滚动并且首位的内容要相接。

满足需求的改写

在上面的代码基础上,我们接下来要完成下面这几件事情就可以满足我们的需求

  1. 让它能每隔一段时间滚动到下一个页面。
  2. 让初始的页面左右两边都有露出的前后页。
  3. 让自动滚动首位相接,滚动到最后一个后可以看到第一个露出的头。
  4. 当手指放到上面的时候停止滚动,抬起的时候恢复自动滚动。
  5. 当离开该页面的时候停止滚动,进来的时候恢复滚动。

接下来我们来分三个步骤来完成

第一步

第一个问题很好解决,我们可以写个定时器,我这里使用Handle的delay来实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class MyWeakReferenceHandler extends WeakRefrenceHandler<MainFragment1>{

public MyWeakReferenceHandler(MainFragment1 ref) {
super(ref);
}

@Override
protected void handleMessage(MainFragment1 ref, Message msg) {
switch (msg.what){
case HAND_BANNER_DELAY_SCROLL:
int position = ref.mScrollPick.getCurrentItem();
ref.mScrollPick.smoothScrollToPosition(position + 1);
removeMessages(HAND_BANNER_DELAY_SCROLL);
sendEmptyMessageDelayed(HAND_BANNER_DELAY_SCROLL, BANNER_DELAY_TIME);
break;
}
}
}

上面我使用的一个弱引用的Handle每隔一段时间发送消息,接收到消息后就获取当前位置然后加1向后移动。

第二步

第二个问题我是通过前后补数据来完成的,这样既简单而且可以资源重复利用

图片切换示意图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
protected void initData() {
mScrollBanners.add(new ScrollBanner("#f24fc9"));
mScrollBanners.add(new ScrollBanner("#bd60f5"));
mScrollBanners.add(new ScrollBanner("#6a63e6"));
mScrollBanners.add(new ScrollBanner("#df3b3b"));
mScrollBanners.add(new ScrollBanner("#f1e599"));

ScrollBanner last = mScrollBanners.get(mScrollBanners.size() - 1);
mScrollBanners.add(mScrollBanners.get(0));
mScrollBanners.add(mScrollBanners.get(1));
mScrollBanners.add(0, last);
mScrollAdapter.notifyDataSetChanged();
}

在Handle中等滚动到倒数第二个数据的时候,我们瞬间切换到第二个数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void handleMessage(MainFragment1 ref, Message msg) {
switch (msg.what){
case HAND_BANNER_DELAY_SCROLL:
int position = ref.mScrollPick.getCurrentItem();
if(position == -1) return;
if(ref.mScrollBanners.size() <= 1) return;
if(position == ref.mScrollBanners.size() - 2) {
ref.mScrollPick.scrollToPosition(1);
removeMessages(HAND_BANNER_DELAY_SCROLL);
sendEmptyMessageDelayed(HAND_BANNER_DELAY_SCROLL, 500);
return;
}
ref.mScrollPick.smoothScrollToPosition(position + 1);
ref.sendDelayScrollMsg();
break;
}
}

第三步

我们重写它的onTouchLisenter方法来拦截事件,实现手指悬停停止的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mScrollPick.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
mHandler.removeMessages(HAND_BANNER_DELAY_SCROLL);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
sendDelayScrollMsg();
break;
}
return false;
}
});

重写onPause和onResume来取消Handle消息,发送Handle消息来实现停止和播放,这样就可以了。

修改

上面实现有一个bug,如果用手指滑动到最后一个就会出问题,针对这个问题对上面代码进行了修改。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void handleMessage(MainFragment1 ref, Message msg) {
switch (msg.what){
case HAND_BANNER_DELAY_SCROLL:
int position = ref.mScrollPick.getCurrentItem();
if(position == -1) return;
if(ref.mScrollBanners.size() <= 1) return;
ref.mScrollPick.smoothScrollToPosition(position + 1);
ref.sendDelayScrollMsg();
break;
}
}

实现ScrollPick的滚动监听,将之前Handle中对最后替换位置的判断放到这里,就可以实现自动滚动和手动拨动的位置计算了。

1
2
3
4
5
6
7
8
9
 mScrollPick.addOnItemChangedListener(new DiscreteScrollView.OnItemChangedListener<RecyclerView.ViewHolder>() {
@Override
public void onCurrentItemChanged(@Nullable RecyclerView.ViewHolder viewHolder, int adapterPosition) {
XLog.d("adapterPosition == " + adapterPosition);
if(adapterPosition == mScrollBanners.size() - 2) {
mScrollPick.scrollToPosition(1);
}
}
});