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

摘要

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

一场APP界面

滑动动画

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

基础使用

gradle.build中引入库

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

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

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

在代码中进行基本设置

 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());

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

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()));
    }
}

初始化一些测试数据

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来实现的

 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向后移动。

第二步

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

图片切换示意图

    @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中等滚动到倒数第二个数据的时候,我们瞬间切换到第二个数据

@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方法来拦截事件,实现手指悬停停止的效果

 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,如果用手指滑动到最后一个就会出问题,针对这个问题对上面代码进行了修改。

@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中对最后替换位置的判断放到这里,就可以实现自动滚动和手动拨动的位置计算了。

 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);
        }
    }
});