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

Posted -SOLO-

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了300行代码实现循环滚动控件相关的知识,希望对你有一定的参考价值。

序言

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

使用

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

效果

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

原理

通过继承自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行代码实现循环滚动控件的主要内容,如果未能解决你的问题,请参考以下文章

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

使用jQuery实现向上循环滚动效果(超简单)

ListCtrl控件 实现 :程序控制滚动(mfc的)

Android自定义实现循环滚轮控件WheelView

html的滚动字幕怎么实现,请以简单的代码解释为例,谢谢

如何让DataGridview控件自动滚动到指定的行或列