之前工作中做的APP有一个需求,就是要循环滚动轮播一堆图文。所以我就用ScrollView魔改了一下实现了这个需求,以下是Demo的运行画面,数字为0到100的TextView在自动循环轮播:
具体实现如下:
纵向轮播ScrollView:
package ggg.project.dfff;
import android.app.Activity;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ScrollView;
/**
* @author ChenJieZhu
* 监听ScrollView滚动到顶部或者底部做相关事件拦截
*/
public class SmartScrollView extends ScrollView {
private boolean isScrolledToTop = true; // 初始化的时候设置一下值
private boolean isScrolledToBottom = false;
public SmartScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
private SmartScrollChangedListener mSmartScrollChangedListener;
private Thread threadAutoRolling = null;
private boolean threadAutoRollingStart = true, threadAutoRollingPause = false;
private int autoRollingTimeMS = 0;
/** 定义监听接口 */
public interface SmartScrollChangedListener {
void onScrolledToBottom();
void onScrolledToTop();
}
public void setScanScrollChangedListener(SmartScrollChangedListener smartScrollChangedListener) {
mSmartScrollChangedListener = smartScrollChangedListener;
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//if (android.os.Build.VERSION.SDK_INT < 9) { // API 9及之后走onOverScrolled方法监听
if (getScrollY() == 0) { // 小心踩坑1: 这里不能是getScrollY() <= 0
isScrolledToTop = true;
isScrolledToBottom = false;
} else if (getScrollY() + getHeight() - getPaddingTop()-getPaddingBottom() == getChildAt(0).getHeight()) {
// 小心踩坑2: 这里不能是 >=
// 小心踩坑3(可能忽视的细节2):这里最容易忽视的就是ScrollView上下的padding
isScrolledToBottom = true;
isScrolledToTop = false;
} else {
isScrolledToTop = false;
isScrolledToBottom = false;
}
notifyScrollChangedListeners();
//}
}
private void notifyScrollChangedListeners() {
if (isScrolledToTop) {
if (mSmartScrollChangedListener != null) {
mSmartScrollChangedListener.onScrolledToTop();
}
} else if (isScrolledToBottom) {
if (mSmartScrollChangedListener != null) {
mSmartScrollChangedListener.onScrolledToBottom();
}
}
}
public boolean isScrolledToTop() {
return isScrolledToTop;
}
public boolean isScrolledToBottom() {
return isScrolledToBottom;
}
/**ScrollView自动滚动
* @author 陈杰柱 **/
public void startAutoRolling(final Activity activity, final ViewGroup parentView, final int stepPixel, final int stepTimeSpiltMS){
stopAutoRolling();
//设置触底和触顶事件
setScanScrollChangedListener(new SmartScrollView.SmartScrollChangedListener() {
private boolean onScrolledToBottom = false;
@Override
public void onScrolledToBottom() {
synchronized (SmartScrollView.this){
synchronized (parentView){
try{
//即使用户开启了自动滚动,但必须内容比ScrollView长才有必要滚动
if(parentView.getChildCount() <= 0) return;
if(parentView.getHeight() > SmartScrollView.this.getHeight() + 30) {
if(parentView.getChildCount() > 1) {
onScrolledToBottom = true;
View view = parentView.getChildAt(0);
parentView.removeViewAt(0);
parentView.addView(view);
//返回加到容器尾巴的控件之前的像素高度,以防难看的突然跳变和卡住不动
scrollBy(0, -view.getMeasuredHeight());
}else{
View view = parentView.getChildAt(0);
scrollBy(0, -view.getMeasuredHeight());
}
}
onScrolledToBottom = false;
}catch (Exception e){
e.printStackTrace();
}
}
}
}
@Override
public void onScrolledToTop() {
synchronized (parentView){
try{
if(parentView.getChildCount() <=0) return;
if(parentView.getHeight() > SmartScrollView.this.getHeight() + 30) {
if(parentView.getChildCount() > 1){
//17年9月4日,发现会溢出
if(onScrolledToBottom) return;
View view = parentView.getChildAt(parentView.getChildCount() - 1);
parentView.removeViewAt(parentView.getChildCount() - 1);
if (view != null) parentView.addView(view, 0);
//返回加到容器顶部的控件之前的像素高度,以防难看的突然跳变和卡住不动
scrollBy(0, view.getMeasuredHeight());
}else{
// View view = parentView.getChildAt(parentView.getChildCount() - 1);
// scrollBy(0, view.getMeasuredHeight());
}
}
}catch (Exception e){
e.printStackTrace();
}
}
}
});
//开启刷新线程
threadAutoRollingStart = true;
threadAutoRolling = new Thread(new Runnable() {
private Runnable threadRolling = new Runnable() {
@Override
public void run() {
//即使用户开启了自动滚动,但必须内容比ScrollView长才有必要滚动,不做无用功
if(parentView.getHeight() > SmartScrollView.this.getHeight()){
scrollBy(0, stepPixel);
}
}
};
@Override
public void run() {
while(threadAutoRollingStart && !activity.isFinishing()){
try {
//暂停状态开关状态机
if(threadAutoRollingPause){
Thread.sleep(SmartScrollView.this.autoRollingTimeMS);
//状态机状态取反
threadAutoRollingPause = !threadAutoRollingPause;
}
activity.runOnUiThread(threadRolling);
Thread.sleep(stepTimeSpiltMS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
threadAutoRolling.start();
}
/**阻尼:1000为将惯性滚动速度缩小1000倍,近似drag操作。**/
@Override
public void fling(int velocity) {
super.fling(velocity / 1000);
}
/**ScrollView自动滚动停止
* 使用该控件时记得用完销毁时调用该方法
* 关闭自动滚动线程
* 否则回产生线程残留
* @author 陈杰柱 **/
public void stopAutoRolling(){
if(threadAutoRolling != null){
threadAutoRollingStart = false;
try{
threadAutoRolling.interrupt();
}catch (Exception e){
}
}
}
/**ScrollView自动滚动暂停
* @author 陈杰柱 **/
public void pauseAutoRolling(int pauseTimeMS){
autoRollingTimeMS = pauseTimeMS;
threadAutoRollingPause = true;
}
@Override
protected void onDetachedFromWindow() {
stopAutoRolling();
super.onDetachedFromWindow();
}
@Override
public void removeAllViews() {
stopAutoRolling();
super.removeAllViews();
}
@Override
public void removeAllViewsInLayout() {
stopAutoRolling();
super.removeAllViewsInLayout();
}
@Override
protected void removeDetachedView(View child, boolean animate) {
super.removeDetachedView(child, animate);
}
@Override
protected void detachViewFromParent(View child) {
stopAutoRolling();
super.detachViewFromParent(child);
}
@Override
protected void detachViewFromParent(int index) {
stopAutoRolling();
super.detachViewFromParent(index);
}
@Override
protected void detachViewsFromParent(int start, int count) {
stopAutoRolling();
super.detachViewsFromParent(start, count);
}
@Override
protected void detachAllViewsFromParent() {
stopAutoRolling();
super.detachAllViewsFromParent();
}
}
横向轮播ScrollView:
package ggg.project.dfff;
import android.app.Activity;
import android.content.Context;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
/**
* Created by cjz on 2017/9/6.
*/
public class SmartScrollViewHorizontalStyle2 extends HorizontalScrollView{
private boolean isScrolledToTop = true; // 初始化的时候设置一下值
private boolean isScrolledToBottom = false;
public SmartScrollViewHorizontalStyle2(Context context, AttributeSet attrs) {
super(context, attrs);
}
private SmartScrollChangedListener mSmartScrollChangedListener;
private Thread threadAutoRolling = null;
private boolean threadAutoRollingStart = true;
private boolean threadAutoRollingPause = false;
private boolean isBackMoving = false;
private int autoRollingTimeMS = 0;
private class Constant {
public static final int BACK_MOVING = 0;
}
//直接匿名重写(因为没有增加方法所以不用继承)然后生成对象
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case Constant.BACK_MOVING:
scrollBy(-msg.arg2 , 0);
break;
}
super.handleMessage(msg);
}
};
/** 定义监听接口 */
public interface SmartScrollChangedListener {
void onScrolledToBottom();
void onScrolledToTop();
}
public void setScanScrollChangedListener(SmartScrollChangedListener smartScrollChangedListener) {
mSmartScrollChangedListener = smartScrollChangedListener;
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
// if (scrollY == 0) {
// isScrolledToTop = clampedY;
// isScrolledToBottom = false;
// } else {
// isScrolledToTop = false;
// isScrolledToBottom = clampedY;
// }
// notifyScrollChangedListeners();
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
//if (android.os.Build.VERSION.SDK_INT < 9) { // API 9及之后走onOverScrolled方法监听
if (getScrollX() == 0) { // 小心踩坑1: 这里不能是getScrollY() <= 0
isScrolledToTop = true;
isScrolledToBottom = false;
//} else if (getScrollY() + getHeight() - getPaddingTop()-getPaddingBottom() == getChildAt(0).getHeight()) {
} else if (getScrollX() + getWidth() - getPaddingLeft()-getPaddingRight() == getChildAt(0).getWidth()) {
// 小心踩坑2: 这里不能是 >=
// 小心踩坑3(可能忽视的细节2):这里最容易忽视的就是ScrollView上下的padding
isScrolledToBottom = true;
isScrolledToTop = false;
} else {
isScrolledToTop = false;
isScrolledToBottom = false;
}
notifyScrollChangedListeners();
//}
// 有时候写代码习惯了,为了兼容一些边界奇葩情况,上面的代码就会写成<=,>=的情况,结果就出bug了
// 我写的时候写成这样:getScrollY() + getHeight() >= getChildAt(0).getHeight()
// 结果发现快滑动到底部但是还没到时,会发现上面的条件成立了,导致判断错误
// 原因:getScrollY()值不是绝对靠谱的,它会超过边界值,但是它自己会恢复正确,导致上面的计算条件不成立
// 仔细想想也感觉想得通,系统的ScrollView在处理滚动的时候动态计算那个scrollY的时候也会出现超过边界再修正的情况
}
private void notifyScrollChangedListeners() {
if (isScrolledToTop) {
if (mSmartScrollChangedListener != null) {
mSmartScrollChangedListener.onScrolledToTop();
}
} else if (isScrolledToBottom) {
if (mSmartScrollChangedListener != null) {
mSmartScrollChangedListener.onScrolledToBottom();
}
}
}
public boolean isScrolledToTop() {
return isScrolledToTop;
}
public boolean isScrolledToBottom() {
return isScrolledToBottom;
}
public void startAutoRolling(final Activity activity, final ViewGroup parentView, final int stepPixel, final int stepTimeSpiltMS){
stopAutoRolling();
//设置触底和触顶事件
/**ScrollView自动滚动
* @author 陈杰柱 **/
setScanScrollChangedListener(new SmartScrollViewHorizontalStyle2.SmartScrollChangedListener() {
private boolean onScrolledToBottom = false;
@Override
public void onScrolledToBottom() {
synchronized (parentView){
//即使用户开启了自动滚动,但必须内容比ScrollView长才有必要滚动
if(parentView.getChildCount() <= 0) return;
if(parentView.getWidth() > SmartScrollViewHorizontalStyle2.this.getWidth() + 5) {
//控件只有多个时
if(parentView.getChildCount() > 1) {
onScrolledToBottom = true;
ImageView imageView = null;
if(!(((String)parentView.getChildAt(0).getTag() != null ? (String)parentView.getChildAt(0).getTag() : "")).equals("isFakeItem")){
View view = parentView.getChildAt(0);
imageView = new ImageView(activity);
imageView.setLayoutParams(view.getLayoutParams());
imageView.setImageBitmap(view.getDrawingCache());
imageView.setTag("isFakeItem");
parentView.removeViewAt(0);
parentView.addView(imageView, 0);
parentView.addView(view);
//返回加到容器尾巴的控件之前的像素高度,以防难看的突然跳变和卡住不动
scrollBy(-view.getMeasuredWidth(), 0);
Log.i("ssvh", "添加了fakeItem");
}
else{
//先把fakeItem确定其滚出去了,再清除
parentView.removeViewAt(0);
// scrollBy(imageView != null ? imageView.getMeasuredWidth() : 0, 0);
Log.i("ssvh","消除了fakeItem");
}
//控件只有一个时 TODO 自滚期间拖动会可能引发抖动
}else{
//方法1:瞬间回滚
// View view = parentView.getChildAt(0);
// scrollBy(-view.getMeasuredWidth(), 0);
//方法2: 缓慢回滚
if(!isBackMoving){
isBackMoving = true;
new Thread(new Runnable() {
@Override
public void run() {
View view = parentView.getChildAt(0);
//这段的暂停时间计算出来有点过长。已解决:原因为没减去已显示出来的像素宽度
pauseAutoRolling(stepTimeSpiltMS * ((view.getMeasuredWidth() - SmartScrollViewHorizontalStyle2.this.getWidth()) / stepPixel) + 1000);
int backStep = ((view.getMeasuredWidth() - SmartScrollViewHorizontalStyle2.this.getWidth()) / stepPixel);
while(backStep-- > 0 && isBackMoving){
Message msg = new Message();
msg.what = Constant.BACK_MOVING;
msg.arg2 = stepPixel;
handler.sendMessage(msg);
try {
Thread.sleep(stepTimeSpiltMS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
isBackMoving = false;
}
}).start();
}
}
onScrolledToBottom = false;
}
}
}
@Override
public void onScrolledToTop() {
synchronized (parentView){
if(parentView.getChildCount() <=0) return;
if(parentView.getWidth() > SmartScrollViewHorizontalStyle2.this.getWidth() + 30) {
if(parentView.getChildCount() > 1){
if(onScrolledToBottom) return;
View view = parentView.getChildAt(parentView.getChildCount() - 1);
parentView.removeViewAt(parentView.getChildCount() - 1);
if (view != null) parentView.addView(view, 0);
//返回加到容器顶部的控件之前的像素高度,以防难看的突然跳变和卡住不动
scrollBy(view.getMeasuredWidth(), 0);
}
}
}
}
});
//开启刷新线程
threadAutoRollingStart = true;
threadAutoRolling = new Thread(new Runnable() {
@Override
public void run() {
while(threadAutoRollingStart && !activity.isFinishing()){
try {
//暂停状态开关状态机
if(threadAutoRollingPause){
Thread.sleep(SmartScrollViewHorizontalStyle2.this.autoRollingTimeMS);
//状态机状态取反
threadAutoRollingPause = !threadAutoRollingPause;
}
activity.runOnUiThread(runnableRolling);
Thread.sleep(stepTimeSpiltMS);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private Runnable runnableRolling = new Runnable() {
@Override
public void run() {
//即使用户开启了自动滚动,但必须内容比ScrollView长才有必要滚动,不做无用功
if(parentView.getWidth() > SmartScrollViewHorizontalStyle2.this.getWidth()){
scrollBy(stepPixel, 0);
}
}
};
});
threadAutoRolling.start();
}
/**ScrollView自动滚动停止
* 使用该控件时记得用完销毁时调用该方法
* 关闭自动滚动线程
* 否则回产生线程残留
* @author 陈杰柱 **/
public void stopAutoRolling(){
if(threadAutoRolling != null){
threadAutoRollingStart = false;
try{
threadAutoRolling.interrupt();
}catch (Exception e){
}
}
}
/**阻尼:1000为将惯性滚动速度缩小1000倍,近似drag操作。**/
@Override
public void fling(int velocity) {
super.fling(velocity / 1000);
}
/**ScrollView自动滚动暂停
* @author 陈杰柱 **/
public void pauseAutoRolling(int pauseTimeMS){
autoRollingTimeMS = pauseTimeMS;
threadAutoRollingPause = true;
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
isBackMoving = false;
pauseAutoRolling(0);
threadAutoRollingPause = false;
return super.onTouchEvent(ev);
}
@Override
protected void onDetachedFromWindow() {
stopAutoRolling();
super.onDetachedFromWindow();
}
@Override
public void removeAllViews() {
stopAutoRolling();
super.removeAllViews();
}
@Override
public void removeAllViewsInLayout() {
stopAutoRolling();
super.removeAllViewsInLayout();
}
@Override
protected void removeDetachedView(View child, boolean animate) {
super.removeDetachedView(child, animate);
}
@Override
protected void detachViewFromParent(View child) {
stopAutoRolling();
super.detachViewFromParent(child);
}
@Override
protected void detachViewFromParent(int index) {
stopAutoRolling();
super.detachViewFromParent(index);
}
@Override
protected void detachViewsFromParent(int start, int count) {
stopAutoRolling();
super.detachViewsFromParent(start, count);
}
@Override
protected void detachAllViewsFromParent() {
stopAutoRolling();
super.detachAllViewsFromParent();
}
}
原理是通过重写触底和触顶的事件回调,使得触底时顶部的View移动到底部,触顶时底部的View移动到顶部,这样就可以使得ScrollView滚动时永远可以循环下去,再做一些微调使得View加到底部或者顶部之后可以回到加回去之前的位置,使得看起来自然一些,就实现这个需求了。
调用示例:
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="ggg.project.dfff.MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<ggg.project.dfff.SmartScrollView
android:id="@+id/ssv_1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ggg.project.dfff.SmartScrollView>
<ggg.project.dfff.SmartScrollViewHorizontalStyle2
android:id="@+id/ssv_2"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:scrollbars="none">
<LinearLayout
android:id="@+id/ll_content_2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
</LinearLayout>
</ggg.project.dfff.SmartScrollViewHorizontalStyle2>
</LinearLayout>
</FrameLayout>
Activity:
package ggg.project.dfff
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
class MainActivity : AppCompatActivity() {
var ssv1 : SmartScrollView ?= null
var ssv2 : SmartScrollViewHorizontalStyle2 ?= null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
ssv1 = findViewById(R.id.ssv_1) as SmartScrollView //声明纵向滚动控件
ssv2 = findViewById(R.id.ssv_2) as SmartScrollViewHorizontalStyle2 //声明横向滚动控件
var ll_content = findViewById(R.id.ll_content) as LinearLayout //滚动控件封住的纵向线性布局
var ll_content_2 = findViewById(R.id.ll_content_2) as LinearLayout //滚动控件封住的容器
//给容器添加100个TextView做测试
var textSize = 40f
for(i in 0..100) {
var textView = TextView(this)
var textView2 = TextView(this)
textView.setTextSize(textSize)
textView2.setTextSize(textSize)
textView.setText(i.toString())
textView2.setText(i.toString() + " ")
ll_content.addView(textView)
ll_content_2.addView(textView2)
}
//每次步进10行像素,每10毫秒步进一次
ssv1!!.startAutoRolling(this, ll_content, 10, 10)
//每次步进10行像素,每100毫秒步进一次
ssv2!!.startAutoRolling(this, ll_content_2, 10, 100)
//点击时暂停滚动2000毫秒
ssv1!!.setOnTouchListener(View.OnTouchListener { v, event ->
ssv1!!.pauseAutoRolling(2000)
false
})
}
override fun onDestroy() {
ssv1!!.stopAutoRolling()
super.onDestroy()
}
}
其中可重点关注这里:
//每次步进10行像素,每10毫秒步进一次
ssv1!!.startAutoRolling(this, ll_content, 10, 10)
//每次步进10行像素,每100毫秒步进一次
ssv2!!.startAutoRolling(this, ll_content_2, 10, 100)
//点击时暂停滚动2000毫秒
ssv1!!.setOnTouchListener(View.OnTouchListener { v, event ->
ssv1!!.pauseAutoRolling(2000)
false
})
本自定义控件是可以允许用户自己选择每次轮播滚动的像素步进量和隔多久步进一次的,而且允许用户暂停。还有OnTouch事件允许用户通过回调实现一下点击事件,例如点击之后暂停轮播一段时间等。
最后,奉上例子工程Demo地址: