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.