Handling Long Button Press

Hi,

Is there any recommendations on handling a long button hold? I am trying to implement a reset on a App, but only want to happen if the user holds a key for a few seconds.

Cheers
Chris
  • You need to use onKeyPressed() and onKeyReleased().

    If you want to decide how to handle the press based on how long it is pressed, you can just track how long it was held down....

    class MyInputDelegate extends Ui.InputDelegate
    {
    hidden var mKeys;

    function initialize() {
    InputDelegate.initialize();
    mKeys = new [Ui has :EXTENDED_KEYS ? 23 : 16];
    }

    function onKeyPressed(evt) {
    var key = evt.getKey();

    //if (mKeys[key] == null) {
    var now = Sys.getTimer();
    mKeys[key] = now;
    //}

    return true;
    }

    function onKeyReleased(evt) {
    var key = evt.getKey();

    // this can happen if view/delegate is popped as the result of an onKey
    // or onKeyPressed event.
    if (mKeys[key] != null) {
    var now = Sys.getTimer();
    var delta = now - mKeys[key];

    Sys.println(Lang.format("Key $1$ held for $2$ms", [ key, delta ]));

    mKeys[key] = null;
    }

    return true;
    }
    }


    If you want to trigger the event while the key is pressed down...

    using Toybox.Application as App;
    using Toybox.Graphics as Gfx;
    using Toybox.Lang as Lang;
    using Toybox.System as Sys;
    using Toybox.Timer as Timer;
    using Toybox.WatchUi as Ui;


    class MyInputDelegate extends Ui.InputDelegate
    {
    hidden var mKeys;
    hidden var mTimer;

    class KeyHeldDelegate
    {
    hidden var mParent;
    hidden var mKey;

    function initialize(parent, key) {
    mParent = parent;
    mKey = key;
    }

    function invoke() {
    mParent.onKeyHeld(new Ui.KeyEvent(mKey, Ui.PRESS_TYPE_ACTION));
    }
    }

    function initialize(view) {
    InputDelegate.initialize();
    mKeys = new [Ui has :EXTENDED_KEYS ? 23 : 16];
    mTimer = new Timer.Timer();
    }

    function onKeyPressed(evt) {
    var key = evt.getKey();

    if (mKeys[key] == null) {
    mKeys[key] = new KeyHeldDelegate(self, key);
    }

    Sys.println(Lang.format("onKeyPressed: $1$", [ key ]));

    // invoke the key delegate in 1 second
    mTimer.stop();

    mTimer.start(mKeys[key].method(:invoke), 1000, false);

    return true;
    }

    function onKeyHeld(evt) {
    var key = evt.getKey();

    mKeys[key] = null;

    Sys.println(Lang.format("onKeyHeld: $1$", [ key ]));
    }

    function onKeyReleased(evt) {
    var key = evt.getKey();

    // if we have a null then the either the key press was handled
    // by another view and we are getting the release event in our
    // view, or the key was held and we've already handled it.
    if (mKeys[key] == null) {
    return true;
    }

    // at this point, we know the given key has been pressed and
    // not held down for more than a second.
    mKeys[key] = null;

    Sys.println(Lang.format("onKeyReleased: $1$", [ key ]));

    mTimer.stop();

    return true;
    }
    }


    You need to be careful to ensure that there is no outstanding timer when you push a new view/delegate. If you don't, then it is possible that onKeyHeld() would be invoked after the new view is active.
  • If I remember correctly, there is an issue on the vivoactive where the onKeyPressed() and onKeyReleased() methods aren't invoked, so this functionality won't work (at least not until the issue is fixed).
  • IIRC, on-press and on-release only works with physical buttons, and not the on-screen buttons on devices like the the va and 630, but I've not tried it. And on some devices, the "long press" is already used (on the fr 23x devices, a long press of "up" is the menu key.)

    Stepping back, you're goal is to trigger a reset of your app. Why not just use the menu or start key, where the user can select "reset" or hit back and just go on? Are all the keys already assigned to something already?
  • Thanks all. I will try the recommendations and let you know how I get on.

    I've been avoiding screen presses (or confirmations) as the app can be used in the pool, and I am worried they may accidentally trigger. Therefore (like the Garmin swimming app) would like to use a long held physical key press.

    Cheers
    Chris
  • onKeyPressed() and onKeyReleased() also don't work on the FR920XT (6.1 firmware). At least all my tests for both apps and widgets have failed on my 920 (but work on the 920XT simulator).

    Also, Travis, can you explain where the circular reference is that would cause the timer memory leak and require the more complicated code that you show?
  • There is no circular reference, and the problem isn't exactly as I stated in my comments. The timer isn't actually leaked.

    The problem that it is possible for multiple delegates to be created, each with its own timer. Since the delegate has no knowledge about when it becomes active or is replaced, it can't know to deallocate its timer without some other part of the application telling it to do so (the view has onShow() and onHide() that can be leveraged for this). You will get an exception when trying to allocate a fourth timer on some devices. By sharing the timer, this problem is avoided. This isn't likely to be an issue for most applications.

    So, in most sane cases, you should be able to do in the more simple way and just put the timer into the delegate.
  • So I thought that I had tested this on a device and had it work at one point, but I must be mistaken. I double-checked all of the hardware I have and none of them seem to invoke onKeyPressed() or onKeyReleased(). Bummer.

    Travis
  • Thanks for checking the onKeyPressed()/onKeyReleased() and the explanation for ending up with multiple timers.

    Can't the whole timer issue be avoided by just using System.getTimer() to timestamp onKeyPressed() and then use the timestamp in onKeyReleased()? For example:

    class EnterKeyDelegate extends Toybox.Watchui.InputDelegate (or BehaviorDelegate) {
    hidden var keyPressedTime = 0;
    function onKeyPressed( evt ) {
    if (evt.getKey() == Ui.KEY_ENTER) {
    keyPressedTime = System.getTimer();
    return true;
    }
    return false;
    }

    function onKeyReleased( evt ) {
    if ((keyPressedTime > 0) && (evt.getKey() == Ui.KEY_ENTER)) {
    var delta = System.getTimer() - keyPressedTime; // ms since last press
    keyPressedTime = 0;
    if (delta > 1000) {
    // We have a hold
    } else {
    // We have a regular press
    }
    return true;
    }
    return false;
    }
    }
  • Can't the whole timer issue be avoided by just using System.getTimer()


    This is exactly what I do in the first snippet above, but I do it for all keys, not just the enter key. The 'problem' is that nothing happens until the key is released.

    If you use a timer, the action happens when the timer expires. Most user interfaces that I'm familiar with behave consistently with the second option; the action happens while the button is still held, not when it is released.
  • Sorry, I've been horrible at carefully reading this thread!

    I totally agree that we want a "hold" event to occur on button press vs button release. A great example is a stopwatch, where you'd like hold to reset the stop watch. When a user keeps holding down the button, they want to get the feedback when the reset occurs (by the stopwatch changing to zero) and then they can release the button. If we only got the "hold" event information on button release, then the user would have to guess if they had held the button down long enough because they would not get any feedback until they released the button.

    What we really, really need API support for BehaviorDelegate hold-based button events for all buttons. This would remove the need for many apps having to implement hold timers or having to use poorer quality keyReleased() based events. If it isn't possible to implement this for all buttons for all devices, then the API could supply info on a button-by-button bases on whether hold events is supported.