supporting both physical button and touch input

I find it very hard to support "correct"/user friendly input for an app across different devices.

On example:

I made it work with behaviordelegate. As an example let's look at onSelect. Works fine on devices with physical select button.

But/and It also "works" on devices without select button. For example on edge devices (ee2) a touch of the screen generates onSelect. This could be OK, though I don't like it.

And on watches ie: fr965 that both are touch screen and have a physical select button it kind of works: either I click the button or when I touch the screen anywhere an onSelect event is generated.

However I'd like to be more user friendly and be able to know where the screen was tapped, and react accordingly. This could be achieved by disabling the onSelect (for example by removing the onSelect method using an annotation) and thus onTap will be called when the screen is touched, which is good, but when a user clicks the physical button then nothing happens.

Ideally I would hope that I could disable the translation of taps to onSelect from the code. Or onTap should be called before onSelect. But to my knowledge none of this is possible. Are they?

So the only way I can think of supporting "both" touch and physical buttons is by adding a stupid, unnecessary user setting where they can disable the physical buttons. When they chose this setting then onSelect would return false => onTap will be called.

Does anyone have a good solution for this?

  • Right, So I revisited this now, and modified my code (that implements "my*" definition of touch only :) And I came up with the following: based on what was said here + tests in simulator I now dropped the distinction between the different "touch types". I now only look at System.getDeviceSettings().isTouchScreen and if it's true then:

    1. I add a menu in the on-device settings to enable/disable touch

    2. when disabled then besides the above I also added new code now for onKey to cover the ENTER key for venu, va devices:

    public function onKey(keyEvent as KeyEvent) as Boolean {
      if (keyEvent.getKey() == KEY_ENTER && action == null) {
        // on devices that have ENTER key but it's not mapped to SELECT behavior (i.e: venusq, vivoactive3)
        log("onKey: " + keyEvent.getKey() + ", " + keyEvent.getType());
        action = Select;
      }
      return doAction(false);
    }
    *) In Hiker when user disables touch it means that the touch won't start/stop the activity (because this is what users reported, that their shirt sleeves accidentally stopped the session) but the rest of the touch functionality will not be blocked. I'll see what the feedback is. In theory they can still swipe Lap accidentally, but then the activity will still continue which I guess lessens the risk.
  • Understand that if you do the common thing and display a menu when recording is active (resume/save/discard), your setting will make no difference on input to that menu.  Same with MapView and MapTrackView if you want to use maps

  • Sounds good to me! Simpler is usually better. (This statement will be rendered super ironic by the following wall of text. Sorry :/)

    TL;DR on real watches, in native activities:

    - tap never starts/stops an activity (afaik)

    every watch uses a physical button press to start/stop the activity timer

    - right-swipe does not take a lap on any device afaik.

    If it does so in your app, I assume it's because you assume that onBack() means the user wanted to take a lap (and right-swipe is mapped to onBack() in touchscreen devices.)

    - To further the previous point, onBack() does not always mean the user took a lap.

    fr630 has separate buttons for back and lap. va3 takes a lap by double-tapping the screen. va3 is the only watch I know of that uses touch to take laps


    If it were me, I would simply not allow an activity to be started or stopped with a tap, especially since that's how native activities work (they don't use tap to start/stop the timer). As far as I know, every Garmin watch, regardless of being "touch optional" (5-button FR/fenix) or "touch first" (Venu/Vivoactive), requires the use of a physical button to start or stop an activity (*).

    Further to this point, Garmin recently changed the behaviour of the built-in stopwatch and timer (at least on 5-button touchscreen watches) so that none of the controls (such as start/stop) work with the touch screen.

    But obviously it's your app and it's up to you.

    [*] Since Vivoactive 3 series has only 1 button, and outside of activities this button is used to return to the watchface, any kind of widget that implements some sort of stopwatch or timer would need to use a tap to start or stop the timer. I learned this the hard way when I tried to respond to onKey(KEY_ENTER) in a vivoactive3 stopwatch widget and it worked in the sim, but users reported that it didn't work on a real device (because widgets can't use the button).

    Speaking of non-standard behaviour and using the touchscreen to take laps:

    - My VA3 stopwatch widget uses tap to start/stop the timer (since the sole button cannot be used in a widget) and right swipe to take a lap (since implementing the standard double-tap to take lap would mean that the app would have to delay reacting to a single tap to stop the timer)

    - My VA3 stopwatch device app works like native activities: button press to start/stop timer, double tap on screen to take laps

    - Some va3 users of the device app actually wanted a mode where the first button press would start the timer (just like activities), subsequent presses would take a lap (non standard behaviour), and a double tap would stop the timer (non standard behaviour). They thought that taking laps via double tap was too unreliable.

    I think Garmin realized they messed up by designing the VA3 to only have one button, which is why VA4 has two buttons and no other Garmin watch (afaik) only has a single button.

    In theory they can still swipe Lap accidentally, but then the activity will still continue which I guess lessens the risk.

    When you say "swipe Lap", do you mean right swipe? Do you explicitly handle right swipe in your app (and respond by taking a lap) or do you rely on the onBack() behaviour to catch all cases (touch / button)? This highlights the fact that onBack() is not necessarily synonymous with taking a lap, even though the lap and back actions are triggered in the same way on many, but not all, devices.

    As far as I know:

    - no device uses right swipe to take a lap in built-in activities (pls correct me if I'm wrong though)

    - Since Vivoactive 3 only has one button (which is used to start/stop the timer), double-tapping the screen takes a lap during activities. I think VA3 really is the Garmin unicorn as far as it comes to touchscreen behaviour, due to having fewer buttons than any other Garmin. When I implemented a stopwatch device app and widget, VA3 was the only watch which had a significantly input scheme than the other devices, and the only watch which had a different input scheme between the device app and the widget.

    My 2 cents is that (speaking of native activities):

    - right swipe for lap would be nonstandard behaviour

    - the only watch which uses touch to take laps is VA3 (double tap screen)

    (ofc your app doesn't have to be just like native activities, but it doesn't hurt to make the input scheme as close as possible)

    Hopefully I'm not forgetting / being ignorant of some other weird Garmin watch.

    (Btw, the weird input scheme of va3 kinda highlights at least one situation where relying on the built-in behaviours is better than trying to reinvent the wheel by detecting specific buttons / touches. Too bad that - as mentioned in another thread - the behaviors are triggered before the keys/touches, but that's probably by design.)

    Of course if you do want to mimic standard activity behaviour for taking laps, it would be a huge pain as you'd have to hardcode detection for various inputs like:

    - lap key / back key for all devices. (Note that some devices - like fr630 - have separate dedicated lap and back buttons, so for those devices, you definitely do not want the back key to take laps.)

    - double tap for VA3

    (actually, that's probably just about it - but ofc you'd have to reinvent the wheel for the double tap detection)

    If you only support newer devices (like CIQ 4+), some of these weird cases go away, as they apply to super old devices like fr630 and va3.

  • on any devices I've used (many in the last decade), you can use onKey looking for KEY_ENTER or KEY_START (depends on the device) so that onSelect isn't used but buttons are...

    I do the same on 5 buttons and touch for a number of apps and require the button press to open and close my garage door for example

  • on any devices I've used (many in the last decade), you can use onKey looking for KEY_ENTER or KEY_START (depends on the device) so that onSelect isn't used but buttons are...

    Yes I think we all realize that.

    The point of most of the above comments is not *how* to accomplish this stuff, but *what* the right thing to do is, for handling input in a device app that records an activity.

    e.g.

    - using onSelect() to start/stop the timer is the wrong thing to do (if you care to emulate native activities), since a tap triggers onSelect() on touchscreen devices, and a tap doesn't start/stop the activity timer on any device.

    - using onBack() to take laps is the wrong thing to do (if you care to emulate activities), since right swipe triggers onBack() on all touchscreen devices, and right swipe doesn't take laps on any device. Furthermore, some devices (like fr630) have a dedicated lap button which is separate from the back button

    But first the decision has to be made as to how closely the app dev wants to emulate native activities.

    For example, va3 uses double-tap to take a lap (not right swipe), but the app would have to detect a double tap manually.

  • And I said early on that the best way is to use the settings in the FW to turn touch on or off. That is exactly how my apps work.  Users want things to be familiar when it comes to input in any app

    How do your apps in the store do things?

  • yes, you guessed it. I don't really have a "special treatment" for laps. But because my own watch in a 5 button watch I just have the following logic. Which probably is not very good, so I'll try to improve it:

    onBack() {
        if (timerActive) {
            session.addLap();
            return true;
        } else {
            if (hasStarted) {
                WatchUi.pushView(new ExitMenu(), new ExitMenuDelegate());
                return true;
            } else {
                return false;
            }
        }
    }

  • And I said early on that the best way is to use the settings in the FW to turn touch on or off. That is exactly how my apps work.  Users want things to be familiar when it comes to input in any app

    I actually agree with you 100% so I don't know what point you're trying to make here.

    The point I was trying to make is that I think flocsy knows that you can use onKey() to check for KEY_ENTER instead of onSelect() (for example) - the "how" (implementation) in that specific case isn't very complicated.

    What I think you're missing is *why* flocsy has chosen to do so.

    I think the real question for handling input is "what" the app should do (focusing on the design of the behaviour users actually see, not necessarily the details of the code that's written - the "how"):

    1) emulate native activities (lots of work, some edge cases like vivoactive 3 and fr630). i.e. As you say, do things in a way to maximize familiarity for the users. This is the approach I would take.

    2) have a simpler implementation like have onSelect() start/stop activities and onBack() take laps even if it's not what all of the real watches do. This seems to be the approach flocsy has taken. (As he said, it works ok for his 5 button watch, although as I said, there are a few ways his app works differently than native activities, even for 5 button touchscreen watches but especially for touchscreen watches with less than 5 buttons.)

    In my opinion, the choice of implementation should follow the design decision and not the other way around.

    i.e. If It were me, I would first decide what the app should do for input, then ask myself how does that work [in terms of what functions to call, how to distinguish between devices, etc.]

    Yes, the complexity of the actual implementation may inform the design decision, as it apparently did in this case, but that nuance wasn't captured by saying that you can use onKey instead of onSelect for that one situation, which again should be obvious to all of us in this thread.

  • How do your apps in the store do things?

    I think I already explained that in at least one case (stopwatch device app), I went to a *lot* of trouble to try to emulate the way native activities work on the device, even though the code isn't necessarily simple and the device behaviour may not be straightforward (e.g. FR630 and Vivoactive 3 quirks mentioned several times).

    In fact, to capture the nuances of fr630 and vivoactive3 in the the app, I actually have exclude annotations which specify:

    - whether the device has a dedicated back button (only fr630 has a *dedicated* back button which isn't also a lap button)

    - whether the device has a back button at all (only vivoactive3 lacks a back button)

    I don't have a setting to disable touch in that app because it's not really appropriate or needed in that case. [Again, I agree with you on that point.]

  • yes, you guessed it. I don't really have a "special treatment" for laps. But because my own watch in a 5 button watch I just have the following logic. Which probably is not very good, so I'll try to improve it:

    Yeah like I said that's your decision but keep in mind it's different from native behaviour in the sense that:

    1) it will be triggered by right swipe on most touch screen watches (except super old ones like vivoactive, vivoactive_hr and fr630 which use left/right swiping for changing pages), but right swipe doesn't take laps in activities on any device, as far as I know

    2) vivoactive3* - which only has 1 button - uses double-tap for laps (in activities). Ofc it's not trivial to handle this case, as you need to hardcode the implementation for vivoactive3* and write the code to detect the double-tap

    3) fr630 has separate dedicated buttons for back and lap, so in that case, you actually need to check for KEY_LAP (and not KEY_BACK or onBack()) when you want to take a lap. Again this implementation would have to be hardcoded for fr630. fr630 is a weird case of a 5-button touchscreen watch, but none of the buttons are used for scrolling, and the physical layout of the buttons is different than all the other 5-button watches. (The 2 "extra" buttons are the dedicated lap button and the dedicated menu button.)

    Yes it's a pain to handle 2 and 3, but most devs can solve that by simply not supporting those super old watches haha.

    1 can be handled for most watches by taking laps when KEY_ESC is seen instead of in onBack().

    It's too bad the Garmin behavior delegate design doesn't support more behaviour type and reporting multiple behaviours at the same time. Garmin could've implemented something like onBehavior() where BEHAVIOR_BACK and BEHAVIOR_LAP could be triggered for a single input (in a single call of the callback), for devices which do have a combined back and lap button (which is most watches). Then all the app would have to do is check for BEHAVIOR_LAP, without needing to worry about the nuances of double tap or a dedicated lap button.

    For that matter, it would be more flexible if there was a single callback function like onInput() which would give all the behaviors and the key / swipe gesture for any given input, so the dev could decide exactly what to do based on how the behavior was triggered (without needing to hardcode certain mappings of input to behavior). That would solve cases that have come up recently, like when a dev wants to handle a right swipe differently from onBack (when onBack is triggered by a button).