300行代码实现循环滚动控件
Posted -SOLO-
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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;
最后所有的功能只依赖上述两个类,关于动画的时长写死在类中的,没有抽成方法。有需要的自己去改吧。
以上是关于300行代码实现循环滚动控件的主要内容,如果未能解决你的问题,请参考以下文章