The preface

In the business needs to display a cyclic scrolling control, content can be cyclic scrolling, can automatically scroll, finger touch will pause. Because the current program is based on ViewPager or RecycleView. You also need to implement Adapters, and you need to intercept various events. The use cost is relatively high. So I created a custom control to do that,

use

It’s easy to use. Just place the controls you want to display in it. Just like normal HorizontalScrollView usage. But the child controls mustLoopLinearLayout

The effect

  • 1. Support left and right circular scrolling
  • 2. Automatic scrolling is supported
  • 3. Support click events
  • 4. Touch pause
  • 5. Support inertial rolling
  • 6. Less than 300 lines of code, simple logic and easy to expand

The principle of

By inheriting the HorizontalScrollView implementation, the re-onOverscrolled and scrollTo methods judge whether the boundary has been reached before calling the supper method. If arrive you call LoopLinearLayout. ChangeItemsToRight () method to put the content.

Child-.layout () is used with no performance problems. Once placed, scrollX is reassigned.

It is important to note that inHorizontalScrollViewOne of them is responsible for inertial rollOverScroller But it’s calling itflingMethod is preceded by maxX, which prevents scrolling out of the content of the control. So I modified this class with reflection. interceptedflingmethods

The animation duration is set to how long it takes to scroll the width of a LoopScrollView. Also, infinite looping animations need to be removed from onDetachedFromWindow to avoid memory leaks

The source code

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

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(a) {
        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(a) {
        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 rolling again
            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();
                // Unable to scroll to the right, move the off-screen item to the back
                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) {
                            // indicates that subsequent controls need to be removed
                            needRemove = true; }}else{
                        int itemRight = itemView.getRight() + params.rightMargin;
                        if (itemRight <= scrollX) {
                            // indicates that subsequent controls need to be removed
                            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){
            // indicates that the left stroke is not moving
            return  false;
        }
        View child = getChildAt(0);
        if(child ! =null) {
            int childWidth = child.getWidth();
            return getWidth() + scrollX < childWidth + getPaddingLeft() + getPaddingRight();
        }
        return false; }}Copy the code

In the end, all functions only depend on the above two classes. The duration of animation is written in the class, and there is no method of drawing. Change it yourself if you need to.