美文网首页
SweetCircularView循环滑动组件

SweetCircularView循环滑动组件

作者: a惊叫唤 | 来源:发表于2018-05-15 21:18 被阅读0次

在项目开发中,经常有首页轮播展示的需求,通常我们使用ViewPager就能满足需求。但经常随着需求变动,样式或者动画的修改,这时ViewPager往往改起来有点复杂了,并且要循环滑动时是最麻烦的。今天在这里为大家推荐一个专门为Banner设计的组件SweetCircularView,并且耦合度相当低,一个类可以直接提取出来使用。

详细介绍一下这个组件强大的功能支持,弥补了ViewPager在作为Banner时的功能缺陷。天生支持以下两个重要的特性:

  • 循环滑动
    手势/定时自动循环滑动啦,使用BaseAdapter实现内部视图复用,减少内存消耗滑动卡顿等问题。
    可配置属性:自定义滑动动画,手势快速滑动,滑动方向垂直或水平。
  • Item缩进
    缩进中心视图,并且展示左右视图,类似与PC网易云音乐首页Banner的样式。
    image.png
    当然除了以上两个属性意外,基本的Banner空间特性肯定是有的,比如弹性归位,惯性滑动,点击选中等等。附上组件源码:agility2/SweetCircularView,好用的话别忘了加颗闪亮的星星哦✨✨✨✨✨

下面来简单的介绍一下组件实现的基本原理,首先基本思路是给予Adapter的视图复用机制减少内存开销,然后重写onTouchEvent,onDispatchEvent,onInterceptTouchEvent,实现手势滑动相关逻辑,在滑动的时候将动画组件分离结偶,方便以后定义滑动动画,最后收尾的是视图滑动之后的停靠逻辑。

  • Adapter的视图复用,先贴关键代码:
    public SweetCircularView setAdapter(BaseAdapter cycleAdapter) {
        if (adapter != null) {
            adapter.unregisterDataSetObserver(dataSetObserver);
        }
        if (cycleAdapter != null) {
            dataSetObserver = new AdapterDataSetObserver();
            cycleAdapter.registerDataSetObserver(dataSetObserver);
        }
        adapter = cycleAdapter;
        if (null != adapter) {
            adapter.notifyDataSetChanged();
        }
        return this;
    }

        void updateView() {
            if (adapter != null && dataIndex >= 0 && dataIndex < adapter.getCount() && state == NONE) {
                state = USING;
                View convertView = adapter.getView(dataIndex, view, SweetCircularView.this);
                if (convertView == view) {
                    // nothing to do
                } else {
                    // remove old view
                    removeView();
                    // add new view
                    if (convertView != null) {
                        if (convertView.getParent() != SweetCircularView.this) {
                            addView(convertView);
                        }
                    }
                }
                //  ...
            }
        }

在新设置或调用adapter.notifyDataSetChanged时触发requestLayout使视图重新布局,在重新布局时先去出对应已经被添加的子视图使用adapter.getView进行视图刷新或创建,流程与ListView.setAdapter相同。

  • 重写onTouchEvent,onDispatchTouchEvent,onInterceptTouchEvent
    这三个方法是手势滑动的关键,主要思想是:首先在dispatch中判断事件是否需要进行拦截,在通过intercept返回true进行拦截,使事件进入onTouch完成移动。
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
        boolean superState = super.dispatchTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                needIntercept = false;
                lastPoint.set(event.getX(), event.getY());
                // 禁止父视图中的触摸事件,使事件派发到当前视图中
                // 处理ListView,ScrollView 嵌套的手势事件派发问题
                getParent().requestDisallowInterceptTouchEvent(true);
                return true;// can not return superState.
            case MotionEvent.ACTION_MOVE:
                float absXDiff = Math.abs(event.getX() - lastPoint.x);
                float absYDiff = Math.abs(event.getY() - lastPoint.y);
                if (orientation == LinearLayout.HORIZONTAL) {
                    if (absXDiff > absYDiff && absXDiff > MOVE_SLOP) {
                        // 当手指垂直或水平移动距离大于移动阀值时,确定为需要拦截处理
                        needIntercept = true;
                    } else if (absYDiff > absXDiff && absYDiff > MOVE_SLOP) {
                        // restore touch event in parent
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                } else if (orientation == LinearLayout.VERTICAL) {
                    // 垂直滑动模式下 使用Y值
                    if (absYDiff > absXDiff && absYDiff > MOVE_SLOP) {
                        needIntercept = true;
                    } else if (absXDiff > absYDiff && absXDiff > MOVE_SLOP) {
                        // restore touch event in parent
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                }
                // pause auto switch
                interceptAutoCycle();
                return superState;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                // ......
        }
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
        boolean superState = super.onInterceptTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                // 返回是否进行拦截,拦截的事件进入onTouchEvent 
                return needIntercept;
                // ......  其它 case 不拦截
        }
}
@Override
public boolean onTouchEvent(MotionEvent event) {
        boolean superState = super.onTouchEvent(event);
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
               // ......
            case MotionEvent.ACTION_MOVE:
                // .......
                // 最终调用 move方法进行移动
                    if (orientation == LinearLayout.HORIZONTAL && absXDiff > absYDiff) {
                        move((int) -xDiff);
                    } else if (orientation == LinearLayout.VERTICAL && absYDiff > absXDiff) {
                        move((int) -yDiff);
                    }
                }
               // ......
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (isMoving) {
                    // 使用VelocityTracker获取手指离开时的滑动速度
                    if (LinearLayout.HORIZONTAL == orientation) {
                        offset = getScrollX();
                        velocity = velocityTracker.getXVelocity();
                    } else {
                        offset = getScrollY();
                        velocity = velocityTracker.getYVelocity();
                    }
                    // 根据手指离开视图时的速度计算惯性距离
                    int inertialDis = -(int) (velocity * durationOnInertial * inertialRatio);
                    if (Math.abs(inertialDis) + Math.abs(offset) <= maxOffset) {
                        inertialDis = 0;
                    }
                    // 开始自动滑动惯性距离
                    autoMove(inertialDis, durationOnInertial, new Runnable() {
                        @Override
                        public void run() {
                            // 自动滑动(惯性)之后停靠
                            autoPacking();
                        }
                    });
                }
                velocityTracker.clear();
                break;
            default:
                return superState;
        }
        return true;
}

进行真是移动的move方法,由于是视图复用机制,所以需要在滑动的同时去更新视图的信息,更新视图基于中心视图向左右或上下两边延伸。

protected final void move(final int offset) {
        isMoving = true;
        int scrolled, maxOffset;
        if (orientation == LinearLayout.VERTICAL) {
            scrollBy(0, offset);
            scrolled = getScrollY();
            maxOffset = getItemHeight() + spaceBetweenItems;
        } else { // HORIZONTAL
            scrollBy(offset, 0);
            scrolled = getScrollX();
            maxOffset = getItemWidth() + spaceBetweenItems;
        }
        notifyOnItemScrolled(offset);
        final int overOffset = Math.abs(scrolled) - maxOffset;
        if (overOffset >= 0) {
            final int size = getRecycleItemSize();
            ItemWrapper item;
            if (scrolled > 0) {
                // 右/下滑动,复用视图下标逐个-1
                for (int i = 0; i < size; i++) {
                    item = findItem(i);
                    item.itemIndex -= 1;
                }
            } else if (scrolled < 0) {
                for (int i = size - 1; i >= 0; i--) {
                    item = findItem(i);
                    item.itemIndex += 1;
                }
            }
            // cycleItemIndex:使视图展示内容与adapter中的数据下标进行绑定,形成循环
            for (ItemWrapper tmp : items) {
                tmp.itemIndex = cycleItemIndex(tmp.itemIndex);
            }
            if (orientation == LinearLayout.VERTICAL) {
                scrollTo(0, scrolled > 0 ? overOffset : -overOffset);
            } else { // HORIZONTAL
                scrollTo(scrolled > 0 ? overOffset : -overOffset, 0);
            }
            // 已中心视图作为参考点,向左右/上下两个方向更新视图
            updateAllItemView(getCurrentIndex());
            // 根据参数对齐视图位置和更新大小
            alignAllItemPosition();
        }
}

组件本身将动画效果实现结偶,自定义动画可以使用AnimationAdapter很方便精准的控制,以上大概讲述了组件的核心原理,最后付上链式调用方式,看上去十分简洁,最终的效果就是文章开头截图的效果啦~~

    private final BaseAdapter adapter = new ArrayAdapter() {
        @Override
        public View getView(int i, View view, ViewGroup parent) {
            if (null == view) {
                view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_item, null);
            }
            view.setOnClickListener(v -> logout(TAG, "onClick: [" + i + "]"));
            // TODO ......
            return view;
        }
    };

    private void initGallery(SweetCircularView circular) {
        circular.setAdapter(adapter)
                .setClick2Selected(false)                   // 点击视图选中(将点击的非中心视图移动到中心)
                .setDurationOnInertial(1000)                // 自动滑动到下一视图的动画时间
                .setDurationOnPacking(500)                  // 惯性停靠动画时间
                .setOverRatio(0.2f)                         // 手指滑动停止之后视图归位的越界系数(>20%为滑动到下一个视图)
                .setInertialRatio(0.01f)                    // 惯性滑动速度
                .setAutoCycle(true, true)                   // 自动滑动
                .setIntervalOnAutoCycle(4000)               // 自动滑动间隔
                .setIndent(320, 220, 320, 220)              // 设置中心视图参考与父视图的缩进边距(默认铺满父视图)
                .setAnimationAdapter(new SimpleCircularAnimator().setRotation(20)) // 设置动画适配器
                .setRecycleItemSize(gallery.getRecycleItemSize() + 2) // 设置可复用的视图个数
                .setOrientation(LinearLayout.HORIZONTAL)    // 设置滑动方向(垂直/水平)
                .setSpaceBetweenItems(gallery.getSpaceBetweenItems() - 20) // 设置相邻视图之间的间隙
                // .setIndicator(<T extends IIndicator> T);    // 绑定滑动指示器
                .setOnItemScrolledListener((v, dataIndex, offset) -> logout(TAG, "scrolled: [" + dataIndex + ", " + offset + "]"))
                .setOnItemSelectedListener((v, dataIndex) -> logout(TAG, "selected: [" + dataIndex + "]"));
    }

最后推荐Android快速开发的工具库,Github:agility2
agility2/CommonTools 基础工具类:压缩图片,渲染文字...
agility2/DynamicProxy 动态代理
agility2/FieldUtils 反射
agility2/IOUtils 流处理:对接,写文件...

相关文章

  • SweetCircularView循环滑动组件

    在项目开发中,经常有首页轮播展示的需求,通常我们使用ViewPager就能满足需求。但经常随着需求变动,样式或者动...

  • 列表循环滑动

    很多地方需要滑动列表,比如:排行榜。滑动列表组件: 其中ScrollView,就是控制滑动的组件 Viewport...

  • react 合成事件

    1. 场景 父组件是个左右可滑动的组件,子组件是可左右滑动的图片展示。功能是手指左右滑动时可页面切换,但是在滑动图...

  • 循环滑动

    // //ViewController.m //循环滑动 #import"ViewController.h" #d...

  • 图片列表

    图片分享按钮改造 新建PhotoList 组件,导入路由组件 绘制图片列表组件页面结构 滑动条无法滑动的问题在Ph...

  • 滑动组件

    这是很简单的水平滑动特效,纯css实现,主要用到属性是overflow,可以设置超出部分的显示效果来实现我们想要的...

  • 一个类似探探的小程序高性能卡片滑动组件

    cardSwipe - 小程序卡片滑动组件 介绍 此组件是使用原生微信小程序代码开发的一款高性能的卡片滑动组件,无...

  • 基于scrollView的菜单标题对应tableview的段头标

    一个scrollView联动tableview的组件,网上基本都是横向滑动,因项目需要写了竖向滑动组件,记录一下。...

  • 3. 自定义控件(3)

    自定义组件之滑动开关

  • 小程序《音乐播放器》中组件的使用

    1. swiper组件、scroll-view组件 swiper组件编写滑动页面结构 swiper组件常用属性 i...

网友评论

      本文标题:SweetCircularView循环滑动组件

      本文链接:https://www.haomeiwen.com/subject/dmoydftx.html