300行代码实现循环滚动控件

作者: zhuguohui | 来源:发表于2021-11-30 16:19 被阅读0次

    序言

    在业务中需要显示一个循环滚动的控件,内容可以循环滚动,可以自动滚动,手指触摸的时候会暂停。
    由于目前的方案都是基于ViewPager或者RecycleView的。还需要实现Adapter,需要拦截各种事件。使用成本比较高。于是我就自定义了一个控件实现该功能,

    使用

    使用起来很简单。把需要显示的控件放置在其中就行。就和普通的HorizontalScrollView用法一样。
    不过子控件必须要LoopLinearLayout

    在这里插入图片描述

    效果

    • 1.支持左右循环滚动
    • 2.支持自动滚动
    • 3.支持点击事件
    • 4.触摸暂停
    • 5.支持惯性滚动
    • 6.一共不到300行代码,逻辑简单易于扩展


      p2.gif

    原理

    通过继承自HorizontalScrollView实现,重新onOverScrolledscrollTo 方法在调用supper方法之前,对是否到达边界进行判断,如果到达就调用LoopLinearLayout.changeItemsToRight() 方法对内容重新摆放。

    摆放使用的是 child.layout() 的方法,没有性能问题。摆放完成以后,对scrollX进行重新赋值。

    需要注意的是在HorizontalScrollView中有一个负责惯性滚动的OverScroller

    在这里插入图片描述
    但是在调用其fling方法之前会设置maxX这导致无法滚动到控件内容之外。所以使用反射修改了这个类。拦截了fling方法
    在这里插入图片描述

    而动画的时长设置的是滚动一个LoopScrollView宽度需要的时间。还有就是无限循环的动画需要在
    onDetachedFromWindow中移除,避免内存泄漏

    源码

    LoopLinearLayout

    package com.example.myapplication;
    
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.LinearLayout;
    
    import androidx.annotation.Nullable;
    
    import java.util.List;
    
    /**
     * Created by zhuguohui
     * Date: 2021/11/30
     * Time: 10:46
     * Desc:
     */
    public class LoopLinearLayout extends LinearLayout {
        public LoopLinearLayout(Context context) {
            this(context, null);
        }
    
        public LoopLinearLayout(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
    
        public void changeItemsToRight(List<View> moveItems, int offset) {
    
            int offset2 = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View childAt = getChildAt(i);
                if (!moveItems.contains(childAt)) {
                    MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
                    offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
                    childAt.layout(childAt.getLeft() - offset, childAt.getTop(), childAt.getRight() - offset, childAt.getBottom());
                }
            }
            for(View view:moveItems){
                view.layout(view.getLeft()+offset2,view.getTop(),view.getRight()+offset2,view.getBottom());
            }
        }
        public void changeItemsToLeft(List<View> moveItems, int offset) {
    
            int offset2 = 0;
            for (int i = 0; i < getChildCount(); i++) {
                View childAt = getChildAt(i);
                if (!moveItems.contains(childAt)) {
                    MarginLayoutParams layoutParams = (MarginLayoutParams) childAt.getLayoutParams();
                    offset2 += childAt.getWidth() + layoutParams.leftMargin + layoutParams.rightMargin;
                    childAt.layout(childAt.getLeft() + offset, childAt.getTop(), childAt.getRight() + offset, childAt.getBottom());
                }
            }
            for(View view:moveItems){
                view.layout(view.getLeft()-offset2,view.getTop(),view.getRight()-offset2,view.getBottom());
            }
        }
    
    
    }
    
    

    LoopScrollView

    package com.example.myapplication;
    
    import android.animation.Animator;
    import android.animation.AnimatorListenerAdapter;
    import android.animation.ValueAnimator;
    import android.annotation.SuppressLint;
    import android.content.Context;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    import android.view.View;
    import android.view.animation.LinearInterpolator;
    import android.widget.HorizontalScrollView;
    import android.widget.OverScroller;
    
    import java.lang.reflect.Field;
    import java.util.ArrayList;
    import java.util.List;
    
    public class LoopScrollView extends HorizontalScrollView {
    
        private LoopScroller loopScroller;
        private ValueAnimator animator;
    
        public LoopScrollView(Context context) {
            this(context, null);
        }
    
        public LoopScrollView(Context context, AttributeSet attrs) {
            super(context, attrs);
            setOverScrollMode(OVER_SCROLL_ALWAYS);
            try {
                @SuppressLint("DiscouragedPrivateApi")
                Field field =HorizontalScrollView.class.getDeclaredField("mScroller");
                field.setAccessible(true);
                loopScroller = new LoopScroller(getContext());
                field.set(this, loopScroller);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            super.onLayout(changed, l, t, r, b);
            if(changed||animator==null){
                buildAnimation();
            }
        }
    
        private void buildAnimation() {
            if(animator!=null){
                animator.cancel();
                animator=null;
            }
            animator = ValueAnimator.ofInt(getWidth() - getPaddingRight() - getPaddingLeft());
            animator.setDuration(5*1000);
            animator.setRepeatCount(-1);
            animator.setInterpolator(new LinearInterpolator());
            animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                int lastValue;
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    int value= (int) animation.getAnimatedValue();
                    int scrollByX=value-lastValue;
              //      Log.i("zzz","scroll by x="+scrollByX);
                    scrollByX=Math.max(0,scrollByX);
                    if(userUp) {
                        scrollBy(scrollByX, 0);
                    }
                    lastValue=value;
                }
            });
            animator.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation) {
                    super.onAnimationEnd(animation);
                }
    
                @Override
                public void onAnimationStart(Animator animation) {
                    super.onAnimationStart(animation);
                }
    
            });
            animator.start();
        }
    
        static   class LoopScroller extends OverScroller{
            public LoopScroller(Context context) {
                super(context);
            }
    
          @Override
          public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY, int overX, int overY) {
              super.fling(startX, startY, velocityX, velocityY, Integer.MIN_VALUE,Integer.MAX_VALUE, minY, maxY, 0, overY);
          }
      }
    
    
    
    
        @Override
        protected void onDetachedFromWindow() {
            super.onDetachedFromWindow();
            if(animator!=null){
                animator.cancel();
                animator.removeAllListeners();
                animator = null;
            }
        }
    
        @Override
        protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
            if (userUp) {
                //scroller再滚动
                scrollX=loopScroller.getCurrX();
                int detailX = scrollX - lastScrollX;
                lastScrollX = scrollX;
                if(detailX==0){
                    return;
                }
                scrollX = detailX + getScrollX();
    
            }
            int moveTo = moveItem(scrollX,clampedX);
    
            super.onOverScrolled(moveTo, scrollY, false, clampedY);
        }
    
        boolean userUp = true;
        int lastScrollX = 0;
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (ev.getAction() == MotionEvent.ACTION_UP) {
                userUp = true;
                lastScrollX = getScrollX();
            } else {
                userUp = false;
            }
            return super.onTouchEvent(ev);
        }
        @Override
        public void scrollTo(int x, int y) {
            int scrollTo = moveItem(x, false);
            super.scrollTo(scrollTo, y);
        }
    
    
        private int moveItem(int scrollX, boolean clampedX) {
    
            int toScrollX = scrollX;
    
            if (getChildCount() > 0) {
                if (!canScroll(scrollX,clampedX)) {
                    boolean toLeft=scrollX<=0;
                    int mWidth=getWidth()-getPaddingLeft()-getPaddingRight();
                    //无法向右滚动了,将屏幕外的item,移动到后面
                    List<View> needRemoveViewList = new ArrayList<>();
                    LoopLinearLayout group = (LoopLinearLayout) getChildAt(0);
                    int removeItemsWidth = 0;
                    boolean needRemove = false;
                    for (int i = group.getChildCount() - 1; i >= 0; i--) {
                        View itemView = group.getChildAt(i);
                        MarginLayoutParams params = (MarginLayoutParams) itemView.getLayoutParams();
                        if(toLeft){
                            int itemLeft = itemView.getLeft() - params.leftMargin;
                            if (itemLeft >= mWidth) {
                                //表示之后的控件都需要移除
                                needRemove = true;
                            }
                        }else{
                            int itemRight = itemView.getRight() + params.rightMargin;
                            if (itemRight <= scrollX) {
                                //表示之后的控件都需要移除
                                needRemove = true;
                            }
                        }
    
                        if (needRemove) {
                            int itemWidth = itemView.getWidth() + params.rightMargin + params.leftMargin;
                            removeItemsWidth += itemWidth;
                            needRemoveViewList.add(0,itemView);
                        }
                        needRemove=false;
                    }
                    if(!toLeft){
                        group.changeItemsToRight(needRemoveViewList,removeItemsWidth);
                        toScrollX -=removeItemsWidth;
                    }else{
                        group.changeItemsToLeft(needRemoveViewList,removeItemsWidth);
                        toScrollX +=removeItemsWidth;
                    }
    
                }
    
            }
            return Math.max(0, toScrollX);
        }
    
        private boolean canScroll(int scrollX, boolean clampedX) {
            if(scrollX<0){
                return false;
            }
            if(scrollX==0&&clampedX){
                //表示向左划不动了
                return  false;
            }
            View child = getChildAt(0);
            if (child != null) {
                int childWidth = child.getWidth();
                return getWidth() + scrollX < childWidth + getPaddingLeft() + getPaddingRight();
            }
            return false;
        }
    }
    
    

    最后所有的功能只依赖上述两个类,关于动画的时长写死在类中的,没有抽成方法。有需要的自己去改吧。

    相关文章

      网友评论

        本文标题:300行代码实现循环滚动控件

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