注册

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

序言

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

使用

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

效果

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

在这里插入图片描述

原理

通过继承自HorizontalScrollView实现,重新onOverScrolled 和 scrollTo 方法在调用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;
}
}

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


作者:solo_99
链接:https://juejin.cn/post/7036262704482353160
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册