【问题标题】:Android: How to detect when a scroll has endedAndroid:如何检测滚动何时结束
【发布时间】:2011-01-06 13:38:43
【问题描述】:

我正在使用 GestureDetector.SimpleOnGestureListener 的 onScroll 方法在画布上滚动大位图。当滚动结束时,我想重新绘制位图,以防用户想要进一步滚动......离开位图的边缘,但我看不到如何检测滚动何时结束(用户抬起手指从屏幕上)。

e2.getAction() 似乎总是返回值 2 所以这没有帮助。 e2.getPressure 似乎返回相当恒定的值(大约 0.25),直到最后一次 onScroll 调用,此时压力似乎下降到大约 0.13。我想我可以检测到这种压力降低,但这远非万无一失。

一定有更好的方法:有人可以帮忙吗?

【问题讨论】:

  • 我在 onScroll() 方法中添加了if (e2.getPressure() < 0.15)。正如预期的那样,这有时会检测到滚动的结尾,但并非总是如此。肯定有更好的办法!
  • 我现在已经放弃了 getPressure 方法。太不靠谱了。相反,我正在检查滚动的累积距离并在超过定义的限制时重绘位图。这是更可预测的,但仍远非理想。如果有人对更好的方法有任何建议,我会很高兴听到他们的意见!
  • 在该主题的众多答案中,对我最有用的是来自 Akos Cz、Aron Cederholm 和 Mike 的答案。

标签: android android-canvas smooth-scrolling


【解决方案1】:

这是我解决问题的方法。希望这会有所帮助。

// declare class member variables
private GestureDetector mGestureDetector;
private OnTouchListener mGestureListener;
private boolean mIsScrolling = false;


public void initGestureDetection() {
        // Gesture detection
    mGestureDetector = new GestureDetector(new SimpleOnGestureListener() {
        @Override
        public boolean onDoubleTap(MotionEvent e) {
            handleDoubleTap(e);
            return true;
        }

        @Override
        public boolean onSingleTapConfirmed(MotionEvent e) {
            handleSingleTap(e);
            return true;
        }

        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // i'm only scrolling along the X axis
            mIsScrolling = true;                
            handleScroll(Math.round((e2.getX() - e1.getX())));
            return true;
        }

        @Override
        /**
         * Don't know why but we need to intercept this guy and return true so that the other gestures are handled.
         * https://code.google.com/p/android/issues/detail?id=8233
         */
        public boolean onDown(MotionEvent e) {
            Log.d("GestureDetector --> onDown");
            return true;
        }
    });

    mGestureListener = new View.OnTouchListener() {
        public boolean onTouch(View v, MotionEvent event) {

            if (mGestureDetector.onTouchEvent(event)) {
                return true;
            }

            if(event.getAction() == MotionEvent.ACTION_UP) {
                if(mIsScrolling ) {
                    Log.d("OnTouchListener --> onTouch ACTION_UP");
                    mIsScrolling  = false;
                    handleScrollFinished();
                };
            }

            return false;
        }
    };

    // attach the OnTouchListener to the image view
    mImageView.setOnTouchListener(mGestureListener);
}

【讨论】:

  • 感谢您的帮助。从您的链接所指的 Google Code Android 上的线程来看,返回“true”或“false”的后果似乎有些混乱。我会尝试在 onDown 方法中“返回 true”,然后报告回来。
  • 非常感谢您提供此代码。这似乎是我问题的答案。起初我以为您代码中的关键行是 onDown 方法中的“return true”,但实际上这似乎对我的代码没有任何影响。关键点似乎是,据我所见,GestureDetector 永远不会收到 ACTION_UP 事件的通知,因此(您正确识别)我必须在我的 onTouchListener.onTouch() 方法中包含代码来注意这一点滚动结束时的事件。再次感谢!
  • 有趣...而且很有帮助。但在快速测试中,我似乎在投掷之前得到了一个修饰事件。如果正在发生投掷或滚动刚刚停止,我想做不同的事情。当我检测到修饰时,我如何知道我是否即将获得一个投掷事件,这样我就不会执行正常的滚动结束功能?
  • @GarretWilson 您可以使用 VelocityTracker 来检测运动事件的速度,然后您可以使用它来确定是否即将发生投掷。请参阅developer.android.com/reference/android/view/… 和/或以下帖子stackoverflow.com/questions/10155907/…
  • onTouchEvent中捕获ACTION_UP,确保onFling()返回false。
【解决方案2】:

你应该看看http://developer.android.com/reference/android/widget/Scroller.html。 尤其是这可能会有所帮助(按相关性排序):

isFinished();
computeScrollOffset();
getFinalY(); getFinalX(); and getCurrY() getCurrX()
getDuration()

这意味着您必须创建一个 Scroller。

如果您想使用触摸,也可以使用 GestureDetector 并定义您自己的画布滚动。以下示例正在创建一个 ScrollableImageView,为了使用它,您必须定义图像的测量值。您可以定义自己的滚动范围,并在完成滚动后重新绘制图像。

http://www.anddev.org/viewtopic.php?p=31487#31487

根据您的代码,您应该考虑 invalidate(int l, int t, int r, int b);作废。

【讨论】:

  • 感谢您的建议。 Scroller 看起来好像被设计为响应 Fling 手势开始滚动并继续滚动一段由程序员确定的距离(有点像 HTC 手机上的主屏幕)。但是,我希望屏幕图像随着用户的触摸而移动。我在我的应用程序的 anddev 线程中遵循了想法。然而,据我所知,这个例子只绘制了一次缓冲区图像。我的应用程序有一个可能无限大小的图像,需要在应用程序空闲时重绘(在一个滚动完成之后,另一个滚动开始之前)。
【解决方案3】:
SimpleOnGestureListener.onFling() 

它似乎发生在滚动结束时(即用户放开手指),这就是我正在使用的,它对我很有用。

【讨论】:

  • 感谢您的建议。恐怕我几天都没有时间试用它,但我期待着看看它是否适合我。如果是这样就很整洁了!
  • 遗憾的是,onFling 似乎只在快速手势结束时被调用。我需要重绘,不仅是在一次投掷之后,而且在一个缓慢的精确滚动结束时也是如此。
  • 似乎 onFling 即使在短滚动之后也确实会被调用,但不是每次都被调用,例如如果你慢慢释放它,它不会。在 onFling 中,您需要检测滚动和其他内容的速度。
【解决方案4】:

几个月后回到这个问题,我现在采用了不同的策略:使用 Handler(如在 Android Snake 示例中)每 125 毫秒向应用发送一条消息,提示它检查是否有 Scroll已启动以及自上次滚动事件以来是否已超过 100 毫秒。

这似乎工作得很好,但如果有人能看到任何缺点或可能的改进,我将不胜感激。

相关代码在MyView类中:

public class MyView extends android.view.View {

...

private long timeCheckInterval = 125; // milliseconds
private long scrollEndInterval = 100;
public long latestScrollEventTime;
public boolean scrollInProgress = false;

public MyView(Context context) {
    super(context);
}

private timeCheckHandler mTimeCheckHandler = new timeCheckHandler();

class timeCheckHandler extends Handler{

        @Override
        public void handleMessage(Message msg) {
        long now = System.currentTimeMillis();
        if (scrollInProgress && (now>latestScrollEventTime+scrollEndInterval)) {
                    scrollInProgress = false;

// 滚动结束,在此插入代码

//调用doDrawing()方法

// 在滚动结束的地方重新绘制位图

                    [ layout or view ].invalidate();
        }
        this.sleep(timeCheckInterval);
        }

        public void sleep(long delayMillis) {
            this.removeMessages(0);
            sendMessageDelayed(obtainMessage(0), delayMillis);
            }
    }
}

@Override protected void onDraw(Canvas canvas){
        super.onDraw(canvas);

// 将大缓冲区位图绘制到视图画布上的代码 // 定位以考虑正在进行的任何滚动

}

public void doDrawing() {

// 进行详细(且耗时)绘图的代码 // 放到大缓冲区位图上

// 下面的指令重置 Time Check 时钟 //时钟第一次启动是什么时候 // 主活动在应用启动时调用此方法

        mTimeCheckHandler.sleep(timeCheckInterval);
}

// MyView 类的其余部分

}

在 MyGestureDetector 类中

public class MyGestureDetector extends SimpleOnGestureListener {

@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
        float distanceY) {

    [MyView].scrollInProgress = true;
        long now = System.currentTimeMillis();  
    [MyView].latestScrollEventTime =now;

    [MyView].scrollX += (int) distanceX;
    [MyView].scrollY += (int) distanceY;

// 下一条指令导致调用 View 的 onDraw 方法 // 将缓冲区位图绘制到屏幕上 // 移动以考虑滚动

    [MyView].invalidate();

}

// MyGestureDetector 类的其余部分

}

【讨论】:

  • 虽然这行得通,但 Akos Cz 在 2010 年 9 月 29 日提供的答案(见上文)要简洁得多。不仅代码更简单,而且还避免了等待时间检查的任何不必要的延迟。使用 Akos 的代码可以立即检测到滚动的结束。
【解决方案5】:

我正在研究同样的问题。我看到 Akos Cz 回答了你的问题。我创建了类似的东西,但在我的版本中,我注意到它只适用于常规滚动 - 意思是不会产生一掷千金的滚动。但是,如果确实生成了一个投掷 - 无论我是否处理了一个投掷,那么它都没有检测到“onTouchEvent”中的“ACTION_UP”。现在也许这只是我的实现的问题,但如果是的话,我不知道为什么。

经过进一步调查,我注意到在投掷期间,“ACTION_UP”每次都被传递到“e2”中的“onFling”。所以我想这一定是为什么在这些情况下没有在“onTouchEvent”中处理它的原因。

为了让它对我有用,我只需要调用一个方法来处理“onFling”中的“ACTION_UP”,然后它就适用于两种类型的滚动。以下是我在应用中实施的具体步骤:

-在构造函数中将“gestureScrolling”布尔值初始化为“false”。

-我在“onScroll”中设置为“true”

- 创建了一个方法来处理“ACTION_UP”事件。在该事件中,我将“gestureSCrolling”重置为 false,然后完成了我需要做的其余处理。

-在“onTouchEvent”中,如果检测到“ACTION_UP”并且“gestureScrolling”=true,那么我调用我的方法来处理“ACTION_UP”

-我所做的不同之处在于:我还调用了我的方法来处理“onFling”内部的“ACTION_UP”。

【讨论】:

  • 感谢您提供如此清晰的总结。我最终得到的代码与您的代码非常相似,只是我没有费心在 onFling 中测试 ACTION_UP ...我只是将每个 onFling 调用(无论其 ACTION 类型)视为滚动的结束(假设调用 onFling 时滚动标志为真),就像我对待在 onTouchListener 的 onTouch 调用中检测到的 ACTION_UP 事件一样。
  • 我相信每次投掷都是快速滚动和 ACTION_UP 的结果,所以我认为你是对的,不需要测试 ACTION_UP,但总是假设每次投掷都是结束一个卷轴。使用相同的逻辑,您是否真的需要测试 onFling 中的滚动标志是否为真,因为如果我没记错的话,您不能在没有滚动的情况下进行 Fling,所以我认为您不会需要检查是否设置了滚动标志。
  • 是的,我认为你是对的!但是,我认为我仍然需要滚动标志来检测(在 onTouch 中)滚动太慢而不能被视为一掷的结束,因此需要将语句保留在 onFling 中(以及在 onTouch 中)它将标志设置为 false。
【解决方案6】:

我相信对你来说已经太晚了,但是,我似乎已经找到了解决你最初问题的正确方法,而不是必要的意图。

如果您使用 Scroller/OverScroller 对象进行滚动,则应检查以下函数的返回值。

public boolean computeScrollOffset() 

享受

收割者

【讨论】:

  • 感谢您的建议。正如我上面提到的,Scroller 看起来好像被设计为响应 Fling 手势开始滚动并继续滚动由程序员确定的距离(有点像 HTC 手机上的主屏幕)。但是,我希望屏幕图像随着用户的触摸而移动。
【解决方案7】:

我认为这将按您的需要工作

protected class SnappingGestureDetectorListener extends SimpleOnGestureListener{

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY){
        boolean result = super.onScroll(e1, e2, distanceX, distanceY);

        if(!result){
            //Do what you need to do when the scrolling stop here
        }

        return result;
    }

}

【讨论】:

  • 理论上可能,但在实践中并不能按预期工作:即使在滚动期间您也会收到错误消息。
  • SimpleOnGestureListener 总是返回 false
【解决方案8】:

这对我有用。

我用 onFingerUp() 方法丰富了现有的 GestureDetector.OnGestureListener。该侦听器作为内置的 GestureDetector 执行所有操作,它还可以侦听手指抬起事件(它不是 onFling(),因为它仅在手指抬起并伴随快速滑动动作时才会调用)。

import android.content.Context;
import android.os.Handler;
import android.view.GestureDetector;
import android.view.MotionEvent;

public class FingerUpGestureDetector extends GestureDetector {
    FingerUpGestureDetector.OnGestureListener fListener;
    public FingerUpGestureDetector(Context context, OnGestureListener listener) {
        super(context, listener);
        fListener = listener;
    }

    public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, OnGestureListener fListener) {
        super(context, listener);
        this.fListener = fListener;
    }

    public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler, OnGestureListener fListener) {
        super(context, listener, handler);
        this.fListener = fListener;
    }

    public FingerUpGestureDetector(Context context, GestureDetector.OnGestureListener listener, Handler handler, boolean unused, OnGestureListener fListener) {
        super(context, listener, handler, unused);
        this.fListener = fListener;
    }

    public interface OnGestureListener extends GestureDetector.OnGestureListener {
        boolean onFingerUp(MotionEvent e);
    }

    public static class SimpleOnGestureListener extends GestureDetector.SimpleOnGestureListener implements FingerUpGestureDetector.OnGestureListener {
        @Override
        public boolean onFingerUp(MotionEvent e) {
            return false;
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (super.onTouchEvent(ev)) return true;
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            return fListener.onFingerUp(ev);
        }
        return false;
    }
}

【讨论】:

    【解决方案9】:

    我自己没有这样做,但是查看 onTouch() 时,您总是会得到一个序列 01,所以结束必须是 1 以表示手指抬起。

    【讨论】:

    • ListView 的类似问题:stackoverflow.com/questions/1768391/…
    • 感谢您的建议。我在 View 类的定义中添加了“实现 OnTouchListener”,因此整行显示为“public class MyView extends android.view.View implements OnTouchListener {”。但是,当我点击或滚动时,似乎从未调用 onTouch() 方法。我有一个单独的 GestureDetector 类“扩展 SimpleOnGestureListener”这一事实是否意味着 OnTouchListener 被绕过了?
    • Rob Kent,我才看到你的评论。据我所知,OnScrollListener 仅适用于 ListView。我正在“扩展 android.view.View”的视图上使用绘图画布。你知道我是否可以使用 OnScroll 方法(我已经在使用的 GestureListener 除外)?
    【解决方案10】:

    我不了解 Android,但查看文档似乎 Rob 是对的:Android ACTION_UP constant 尝试从 getAction() 检查 ACTION_UP?

    编辑:e1.getAction() 显示什么?它会返回 ACTION_UP 吗?文档说它保存了初始的向下事件,所以它可能还会在指针向上时通知

    编辑:我只能再想到两件事。你在任何时候都返回假吗?这可能会阻止 ACTION_UP

    我唯一要尝试的另一件事是有一个单独的事件,可能是 onDown,并在 onScroll 中设置一个标志,例如 isScrolling。当将 ACTION_UP 赋予 onDown 并设置 isScrolling 时,您可以做任何您想做的事情并将 isScrolling 重置为 false。也就是说,假设onDown和onScroll一起被调用,getAction会在onDown期间返回ACTION_UP

    【讨论】:

    • 是的,我试过检查 getAction() 但它总是在调用 onScroll 时返回 2。 onGestureListener 识别的唯一“up”事件是 onSingleTapUp; this 不会在 Scroll 结束时返回。
    • 但在这段代码中,他们在 SimpleOnGestureListener 的 onScroll 中监听 ACTION_UP:code.google.com/p/connectbot/source/browse/trunk/connectbot/src/…
    • 谢谢,鲍勃。这是一个谜。我在 onScroll 中插入了Log.i("onScroll", Integer.toString(e2.getAction())); 作为第一条语句,而当我滚动时,LogCat 只显示一串“2”。如果我将 Log 语句放在 if(e2.getAction() == MotionEvent.ACTION_UP) 之后,即使我结束滚动,LogCat 也不会记录任何内容。
    • 对不起,这里只是想消除可能性。见我上面的编辑
    • 再次感谢您的建议。不幸的是,e1.getAction() 只返回 0。
    【解决方案11】:

    我还没有尝试/使用过这个,但是一个方法的想法:

    在每个滚动事件上停止/中断重绘画布等待 1 秒,然后在每个滚动事件上开始重绘画布。

    这将导致仅在滚动结束时执行重绘,因为只有最后一个滚动实际上不会中断以完成重绘。

    希望这个想法对你有帮助:)

    【讨论】:

    • 感谢您的建议。但是,我认为这对我没有帮助。我不想打断重绘,因为我想平滑滚动。我的方法是对缓冲区位图进行详细绘制,然后在 onDraw() 方法中将此位图绘制到视图的画布上。在滚动过程中,我重新绘制位图略微移动以反映滚动。我想要做的是检测滚动何时结束,以便我可以重新绘制缓冲区位图(这很耗时),为下一次滚动做好准备。
    【解决方案12】:

    从 GestureListener API 的 onScroll 事件中提取:link text

    公共抽象布尔 onScroll (运动事件 e1,运动事件 e2,浮动 distanceX, float distanceY) 自:API 1级

    返回 * 如果事件被消费,则为 true,否则为 false

    也许一旦事件被消费,动作就完成了,用户已经将手指从屏幕上移开,或者至少完成了这个 onScroll 动作

    然后,您可以在 IF 语句中使用它来扫描 == true,然后开始下一个操作。

    【讨论】:

    • 谢谢。您是否建议我的代码应该调用 onScroll 方法并检查它返回的内容。如果是这样,我不知道该怎么做。我认为这是系统调用的方法来运行我插入的代码。我对文档中的“returns”一词感到困惑,但假设它们的意思是“return”……换句话说,如果事件已被消耗,我的代码应该在方法结束时返回 true,否则返回 false。请你为我澄清一下好吗?
    • 使用触摸事件的返回作为手指何时抬起的提示!当函数返回 true 时,事件就结束了。所以使用它: If(onScroll == True) { DoThis(); } 否则 { EffectErrorHandler();} 和中提琴!
    • 感谢您的进一步评论,Blue。恐怕我还是不明白你的意思。 onScroll() 方法是 OnSimpleGestureListener 的重写方法(它是“扩展”SimpleOnGestureListener 的类的方法),当手指在屏幕上拖动时由 Android 系统调用。我怎么称呼它?我应该在我的代码中的哪个位置拨打电话?我应该提供什么论据?
    • SimpleOnGestureListener 的 Android 参考令人困惑:OnScroll 部分说该方法“如果事件被消耗,则返回 true,否则返回 false”,但类概述说该类“什么都不做并返回 false所有适用的方法”。
    【解决方案13】:

    我尝试向手势检测器添加额外功能。希望它可以帮助某人更好地利用他的时间...

    https://gist.github.com/WildOrangutan/043807ddbd68978179b7cea3b53c71e8

    【讨论】:

      【解决方案14】:

      如果您使用SimpleGestureDetector 来处理滚动事件,您可以这样做

      fun handleTouchEvents(event: MotionEvent): Boolean {
          if(event.action == ACTION_UP) yourListener.onScrollEnd()
          return gestureDetector.onTouchEvent(event)
      }
      

      【讨论】:

        猜你喜欢
        • 2012-10-30
        • 2012-11-23
        • 2011-04-27
        • 2015-11-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2016-10-20
        • 1970-01-01
        相关资源
        最近更新 更多