【问题标题】:Can I have onScrollListener for a ScrollView?我可以为 ScrollView 设置 onScrollListener 吗?
【发布时间】:2012-05-29 14:38:19
【问题描述】:

我在布局中使用HorizontalScrollView,我需要识别用户已到达滚动的起点和终点。

对于ListView,我试过onScrollListener,可以找到滚动的起点和终点。

我尝试在我的Scrollview 中做同样的事情,但似乎不可能。有没有其他可能的方法来实现我所需要的。

【问题讨论】:

  • 有可能。请参阅 user2695685 的答案。简而言之,onStart 中的以下内容可以解决问题:hsv.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {@Override public void onScrollChanged() {Log.i(TAG,"scroll:"+hsv.getScrollX());}}); in onStart() 其中hsvHorizontalScrollView 有效。
  • 接受任何有用的答案..如果其他人发布您自己的答案..
  • 为什么在 Android 中使用 ScrollView 检测滚动事件如此困难?这太疯狂了。

标签: android listener scrollview


【解决方案1】:

Kotlin 用户正在寻找正常的 ScrollView 实现的解决方案:

作为this answer 的扩展,我创建了一个自定义视图,很好地解决了我的问题。

视图(创建一个新的 Kotlin 文件,在第 1 行维护您的包引用):

import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import android.widget.ScrollView
import kotlin.math.abs


class ScrollViewWithEndFunc (
    context: Context?,
    attrs: AttributeSet?,
    defStyle: Int
) : ScrollView(context, attrs, defStyle) {

    constructor(context: Context?) : this(context, null, 0) {}
    constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) {}

    interface OnScrollListener {
        fun onScrollChanged(scrollView: ScrollViewWithEndFunc?, x: Int, y: Int, oldX: Int, oldY: Int)
        fun onEndScroll(scrollView: ScrollViewWithEndFunc?)
    }

    private var isScrolling = false
    private var isTouching = false
    private var scrollingRunnable: Runnable? = null
    private var onScrollListener: OnScrollListener? = null

    fun setOnScrollListener(onScrollListener: OnScrollListener) {
        this.onScrollListener = onScrollListener
    }

    fun removeOnScrollListener() {
        this.onScrollListener = null
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(ev: MotionEvent): Boolean {
        val action = ev.action
        if (action == MotionEvent.ACTION_MOVE) {
            isTouching = true; isScrolling = true
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (isTouching && !isScrolling) {
                onScrollListener?.onEndScroll(this)
            }
            isTouching = false
        }
        return super.onTouchEvent(ev)
    }

    override fun onScrollChanged(x: Int, y: Int, oldX: Int, oldY: Int) {
        super.onScrollChanged(x, y, oldX, oldY)
        if (abs(oldY - y) > 0) {
            scrollingRunnable?.let { removeCallbacks(it) }
            scrollingRunnable = Runnable {
                if (isScrolling && !isTouching) {
                    onScrollListener?.onEndScroll(this@ScrollViewWithEndFunc)
                }
                isScrolling = false
                scrollingRunnable = null
            }
            postDelayed(scrollingRunnable, 200)
        }
        onScrollListener?.onScrollChanged(this, x, y, oldX, oldY)
     }
    }

XML 视图实现:

<your.package.here.ScrollViewWithEndFunc
        android:id="@+id/scrollview_main_dashboard"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fillViewport="true">

Activity/Fragment 实现:

scrollviewMainDashboard.setOnScrollListener(object : ScrollViewWithEndFunc.OnScrollListener {
            override fun onScrollChanged(scrollView: ScrollViewWithEndFunc?, x: Int, y: Int, oldX: Int, oldY: Int) { }
            override fun onEndScroll(scrollView: ScrollViewWithEndFunc?) {
                /* Scroll ended, handle here */
        })

【讨论】:

    【解决方案2】:

    除了接受的答案外,您还需要保留侦听器的引用并在不需要时将其删除。否则,您将获得 ScrollView 和内存泄漏的空指针异常(在接受答案的 cmets 中提到)

    1. 您可以在您的活动/片段中实现 OnScrollChangedListener。

      MyFragment : ViewTreeObserver.OnScrollChangedListener
      
    2. 当您的视图准备就绪时,将其添加到 scrollView。

      scrollView.viewTreeObserver.addOnScrollChangedListener(this)
      
    3. 不再需要时移除监听器(即 onPause())

      scrollView.viewTreeObserver.removeOnScrollChangedListener(this)
      

    【讨论】:

      【解决方案3】:

      这是我编写的一个派生的 Horizo​​ntalScrollView,用于处理有关滚动和滚动结束的通知。当用户停止主动滚动在用户放手后完全减速时,它会正确处理:

      public class ObservableHorizontalScrollView extends HorizontalScrollView {
          public interface OnScrollListener {
              public void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY);
              public void onEndScroll(ObservableHorizontalScrollView scrollView);
          }
      
          private boolean mIsScrolling;
          private boolean mIsTouching;
          private Runnable mScrollingRunnable;
          private OnScrollListener mOnScrollListener;
      
          public ObservableHorizontalScrollView(Context context) {
              this(context, null, 0);
          }
      
          public ObservableHorizontalScrollView(Context context, AttributeSet attrs) {
              this(context, attrs, 0);
          }
      
          public ObservableHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
              super(context, attrs, defStyle);
          }
      
          @Override
          public boolean onTouchEvent(MotionEvent ev) {
              int action = ev.getAction();
      
              if (action == MotionEvent.ACTION_MOVE) {
                  mIsTouching = true;
                  mIsScrolling = true;
              } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
                  if (mIsTouching && !mIsScrolling) {
                      if (mOnScrollListener != null) {
                          mOnScrollListener.onEndScroll(this);
                      }
                  }
      
                  mIsTouching = false;
              }
      
              return super.onTouchEvent(ev);
          }
      
          @Override
          protected void onScrollChanged(int x, int y, int oldX, int oldY) {
              super.onScrollChanged(x, y, oldX, oldY);
      
              if (Math.abs(oldX - x) > 0) {
                  if (mScrollingRunnable != null) {
                      removeCallbacks(mScrollingRunnable);
                  }
      
                  mScrollingRunnable = new Runnable() {
                      public void run() {
                          if (mIsScrolling && !mIsTouching) {
                              if (mOnScrollListener != null) {
                                  mOnScrollListener.onEndScroll(ObservableHorizontalScrollView.this);
                              }
                          }
      
                          mIsScrolling = false;
                          mScrollingRunnable = null;
                      }
                  };
      
                  postDelayed(mScrollingRunnable, 200);
              }
      
              if (mOnScrollListener != null) {
                  mOnScrollListener.onScrollChanged(this, x, y, oldX, oldY);
              }
          }
      
          public OnScrollListener getOnScrollListener() {
              return mOnScrollListener;
          }
      
          public void setOnScrollListener(OnScrollListener mOnEndScrollListener) {
              this.mOnScrollListener = mOnEndScrollListener;
          }
      
      }
      

      【讨论】:

      • 这似乎比使用 ViewTreeObserver 方法更好。只是因为当您有一个活动时,您使用 ListViews 和 ScrollViews 加载了多个片段(例如 3 个带有滑动选项卡的片段),并且您需要为特定视图注册事件。而如果 ViewTreeObserver 已注册,即使它不是活动视图,它也会触发事件。
      • @ZaBlanc - 你能告诉我如何初始化这个类和我的 Activity 中的监听器,它持有 ScrollView 吗?我已经很好地实现了它,但是监听器还不会返回任何值。
      • 这确实有效!并且不需要 sdk > 23 :)
      【解决方案4】:

      您可以定义一个自定义的 ScrollView 类,并添加一个在滚动时调用的接口,如下所示:

      public class ScrollChangeListenerScrollView extends HorizontalScrollView {
      
      
      private MyScrollListener mMyScrollListener;
      
      public ScrollChangeListenerScrollView(Context context) {
          super(context);
      }
      
      public ScrollChangeListenerScrollView(Context context, AttributeSet attrs) {
          super(context, attrs);
      }
      
      public ScrollChangeListenerScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
          super(context, attrs, defStyleAttr);
      }
      
      
      public void setOnMyScrollListener(MyScrollListener myScrollListener){
          this.mMyScrollListener = myScrollListener;
      }
      
      
      @Override
      protected void onScrollChanged(int l, int t, int oldl, int oldt) {
          super.onScrollChanged(l, t, oldl, oldt);
          if(mMyScrollListener!=null){
              mMyScrollListener.onScrollChange(this,l,t,oldl,oldt);
          }
      
      }
      
      public interface MyScrollListener {
          void onScrollChange(View view,int scrollX,int scrollY,int oldScrollX, int oldScrollY);
      }
      
      }
      

      【讨论】:

        【解决方案5】:

        您可以使用NestedScrollView 代替ScrollView。但是,当使用 Kotlin Lambda 时,它不会知道您想要 NestedScrollView 的 setOnScrollChangeListener 而不是 View 的那个(API 级别 23)。您可以通过将第一个参数指定为 NestedScrollView 来解决此问题。

        nestedScrollView.setOnScrollChangeListener { _: NestedScrollView, scrollX: Int, scrollY: Int, _: Int, _: Int ->
            Log.d("ScrollView", "Scrolled to $scrollX, $scrollY")
        }
        

        【讨论】:

        • 这应该是公认的答案,因为使用 viewTreeObserver 的方法会导致内存泄漏,必须手动删除,否则将添加多个滚动观察者。
        • 我完全同意。切换到NestedScrollView 并设置它的OnScrollChangeListener 解决了我的问题。当我在 ScrollView 的 ViewTreeObserver 上设置 OnScrollChangedListener 时,监听器被无缘无故地调用了多次。
        【解决方案6】:

        如果你想知道一个视图的滚动位置,那么你可以在 View 类上使用如下扩展函数:

        fun View?.onScroll(callback: (x: Int, y: Int) -> Unit) {
            var oldX = 0
            var oldY = 0
            this?.viewTreeObserver?.addOnScrollChangedListener {
                if (oldX != scrollX || oldY != scrollY) {
                    callback(scrollX, scrollY)
                    oldX = scrollX
                    oldY = scrollY
                }
            }
        }
        

        【讨论】:

          【解决方案7】:

          这可能非常有用。 使用NestedScrollView 而不是ScrollView。支持库 23.1 引入了 OnScrollChangeListenerNestedScrollView。 所以你可以做这样的事情。

           myScrollView.setOnScrollChangeListener(new NestedScrollView.OnScrollChangeListener() {
                  @Override
                  public void onScrollChange(NestedScrollView v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
                      Log.d("ScrollView","scrollX_"+scrollX+"_scrollY_"+scrollY+"_oldScrollX_"+oldScrollX+"_oldScrollY_"+oldScrollY);
                      //Do something
                  }
              });
          

          【讨论】:

          • 绝对比使用 getViewTreeObserver() 跟踪您之前是否添加过侦听器更容易。谢谢!
          • 但是如何使用 NestedScrollView 作为水平滚动视图呢?找不到任何资源
          • 如果我想使用水平滚动怎么办?
          • @dazed'n'confused NestedScrollView 是支持库的一部分,它的setOnScrollChangeListener 方法没有最低版本要求。
          • 不要与需要 API 级别 23 的 ViewsetOnScrollChangeListener 混淆
          【解决方案8】:
              // --------Start Scroll Bar Slide--------
              final HorizontalScrollView xHorizontalScrollViewHeader = (HorizontalScrollView) findViewById(R.id.HorizontalScrollViewHeader);
              final HorizontalScrollView xHorizontalScrollViewData = (HorizontalScrollView) findViewById(R.id.HorizontalScrollViewData);
              xHorizontalScrollViewData.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {
                  @Override
                  public void onScrollChanged() {
                      int scrollX; int scrollY;
                      scrollX=xHorizontalScrollViewData.getScrollX();
                      scrollY=xHorizontalScrollViewData.getScrollY();
                      xHorizontalScrollViewHeader.scrollTo(scrollX, scrollY);
                  }
              });
              // ---------End Scroll Bar Slide---------
          

          【讨论】:

          • 请尝试添加一些描述以支持您的代码,这将使每个人都了解击球手
          • 这段代码和另一个answer很相似,不是吗?
          【解决方案9】:

          View 的每个实例都调用getViewTreeObserver()。现在当持有ViewTreeObserver 的实例时,您可以使用addOnScrollChangedListener() 方法向其添加OnScrollChangedListener()

          你可以看到更多关于这个类的信息here

          它让您知道每个滚动事件 - 但没有坐标。不过,您可以通过在侦听器中使用 getScrollY()getScrollX() 来获取它们。

          scrollView.getViewTreeObserver().addOnScrollChangedListener(new OnScrollChangedListener() {
              @Override
              public void onScrollChanged() {
                  int scrollY = rootScrollView.getScrollY(); // For ScrollView
                  int scrollX = rootScrollView.getScrollX(); // For HorizontalScrollView
                  // DO SOMETHING WITH THE SCROLL COORDINATES
              }
          });
          

          【讨论】:

          • 此答案应标记为正确。 hsv.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() {@Override public void onScrollChanged() {Log.i(TAG,"scroll:"+hsv.getScrollX());}}); 在 onStart() 中 hsvHorizontalScrollView 有效。我怀疑同样适用于 ScrollView。
          • 完全正确。这是最好的答案。无需扩展 Horizo​​ntalScrollView。刚刚用 ScrollView 测试,效果很好: final ScrollView sv = (ScrollView) findViewById(R.id.scrollViewArticle); sv.getViewTreeObserver().addOnScrollChangedListener(new ViewTreeObserver.OnScrollChangedListener() { @Override public void onScrollChanged() { Log.d("onScrollChanged Y", String.valueOf(sv.getScrollY())); } });跨度>
          • 你应该先检查ViewTreeObserver.isAlive()
          • 答案中的示例代码引入了内存泄漏。由于该方法是add 而不是set,因此所有侦听器都将被保留,直到显式删除。因此,示例中用作侦听器实现的匿名类将泄漏(连同它所引用的任何东西,即外部类)。
          • 可能是个愚蠢的问题,但 rootScrollView 是什么?
          猜你喜欢
          • 1970-01-01
          • 2011-02-05
          • 2011-04-26
          • 2014-01-02
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-03-08
          • 2013-12-04
          相关资源
          最近更新 更多