Repository: beeware/toga

Issue:
Toga slider widget in android has 1 fewer tick counts than in Windows
**Describe the bug**
The toga.Slider widget when defined with range=(2,20), tick_count=19 instantiates a slider in Windows which has 19 tick values.
However when the same code is built and run for Android (both in the emulator and an actual Android phone), the slider only
has 18 tick values.  In addition ,the property Slider.value returns a value which is 18/19 per tick.

**To Reproduce**
I wrote a simple example which demonstrates the bug clearly.    I build and compiled like this:
    briefcase update android
    briefcase build android
    briefcase run android

Here is the source code from app.py.

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

TIMER_MIN=1
TIMER_MAX=19

class slidetest(toga.App):

    def startup(self):
        main_box = toga.Box()

        self.timer_label=toga.Label(text="Counter: ")
        self.timer_label.style.width=200
        self.timer_label.style.padding=5

        self.timer_slide=toga.Slider(range=(TIMER_MIN,TIMER_MAX),tick_count=(TIMER_MAX-TIMER_MIN+1),on_change=(self.timer_change),
            on_release=(self.timer_set))
        self.timer_slide.style.padding=5
        self.timer_slide.style.padding_top=5
        self.timer_slide.style.width=200
        self.timer_slide.value=10.0

        main_box.add(self.timer_slide)
        main_box.add(self.timer_label)

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()

    def timer_change(self,widget):
        self.timer_label.text="Counter: "+str(self.timer_slide.tick_value)

    def timer_set(self,widget):
        pass

def main():
    return slidetest()


**Expected behavior**
This should produce a slider with 19 tick stops, and return values from 2.0 to 20.0.  

The android build of this code has a slide with 18 tick stops and return values from 2.0 to 19.05263.

Screenshots below just show screen capture of the android emulator with slider full left and full right position.  You can see the resulting tick_value is 1 and 18 respectively.

**Screenshots**
![Screenshot 2021-06-10 203556](https://user-images.githubusercontent.com/46497064/121627164-b42a2f00-ca2b-11eb-80f1-6e2ac35f8d1f.gif)
![Screenshot 2021-06-10 203638](https://user-images.githubusercontent.com/46497064/121627173-b8eee300-ca2b-11eb-9816-c8e1d4cf3e3c.gif)


**Environment:**
 - Operating System: Windows
 - Python version:3.8
 - Software versions:
   - Briefcase: briefcase          0.3.5
   - Toga:
   - toga-core          0.3.0.dev27
   - toga-winforms      0.3.0.dev27
   
arrow              1.1.0
binaryornot        0.4.4
certifi            2021.5.30
chardet            4.0.0
click              8.0.1
colorama           0.4.4
cookiecutter       1.7.3
gitdb              4.0.7
GitPython          3.1.17
idna               2.10
Jinja2             2.11.3
jinja2-time        0.2.0
MarkupSafe         2.0.1
oauthlib           3.1.1
pip                21.1.1
poyo               0.5.0
pycparser          2.20
python-dateutil    2.8.1
python-slugify     5.0.2
pythonnet          2.5.2
setuptools         56.0.0
six                1.16.0
smmap              4.0.0
text-unidecode     1.3
toml               0.10.2
travertino         0.1.3
urllib3            1.26.5
wheel              0.36.2

**Additional context**
Add any other context about the problem here.

Toga slider widget in android has 1 fewer tick counts than in Windows
**Describe the bug**
The toga.Slider widget when defined with range=(2,20), tick_count=19 instantiates a slider in Windows which has 19 tick values.
However when the same code is built and run for Android (both in the emulator and an actual Android phone), the slider only
has 18 tick values.  In addition ,the property Slider.value returns a value which is 18/19 per tick.

**To Reproduce**
I wrote a simple example which demonstrates the bug clearly.    I build and compiled like this:
    briefcase update android
    briefcase build android
    briefcase run android

Here is the source code from app.py.

import toga
from toga.style import Pack
from toga.style.pack import COLUMN, ROW

TIMER_MIN=1
TIMER_MAX=19

class slidetest(toga.App):

    def startup(self):
        main_box = toga.Box()

        self.timer_label=toga.Label(text="Counter: ")
        self.timer_label.style.width=200
        self.timer_label.style.padding=5

        self.timer_slide=toga.Slider(range=(TIMER_MIN,TIMER_MAX),tick_count=(TIMER_MAX-TIMER_MIN+1),on_change=(self.timer_change),
            on_release=(self.timer_set))
        self.timer_slide.style.padding=5
        self.timer_slide.style.padding_top=5
        self.timer_slide.style.width=200
        self.timer_slide.value=10.0

        main_box.add(self.timer_slide)
        main_box.add(self.timer_label)

        self.main_window = toga.MainWindow(title=self.formal_name)
        self.main_window.content = main_box
        self.main_window.show()

    def timer_change(self,widget):
        self.timer_label.text="Counter: "+str(self.timer_slide.tick_value)

    def timer_set(self,widget):
        pass

def main():
    return slidetest()


**Expected behavior**
This should produce a slider with 19 tick stops, and return values from 2.0 to 20.0.  

The android build of this code has a slide with 18 tick stops and return values from 2.0 to 19.05263.

Screenshots below just show screen capture of the android emulator with slider full left and full right position.  You can see the resulting tick_value is 1 and 18 respectively.

**Screenshots**
![Screenshot 2021-06-10 203556](https://user-images.githubusercontent.com/46497064/121627164-b42a2f00-ca2b-11eb-80f1-6e2ac35f8d1f.gif)
![Screenshot 2021-06-10 203638](https://user-images.githubusercontent.com/46497064/121627173-b8eee300-ca2b-11eb-9816-c8e1d4cf3e3c.gif)


**Environment:**
 - Operating System: Windows
 - Python version:3.8
 - Software versions:
   - Briefcase: briefcase          0.3.5
   - Toga:
   - toga-core          0.3.0.dev27
   - toga-winforms      0.3.0.dev27
   
arrow              1.1.0
binaryornot        0.4.4
certifi            2021.5.30
chardet            4.0.0
click              8.0.1
colorama           0.4.4
cookiecutter       1.7.3
gitdb              4.0.7
GitPython          3.1.17
idna               2.10
Jinja2             2.11.3
jinja2-time        0.2.0
MarkupSafe         2.0.1
oauthlib           3.1.1
pip                21.1.1
poyo               0.5.0
pycparser          2.20
python-dateutil    2.8.1
python-slugify     5.0.2
pythonnet          2.5.2
setuptools         56.0.0
six                1.16.0
smmap              4.0.0
text-unidecode     1.3
toml               0.10.2
travertino         0.1.3
urllib3            1.26.5
wheel              0.36.2

**Additional context**
Add any other context about the problem here.



Discussion:

Resolving patch:
diff --git a/src/android/toga_android/widgets/slider.py b/src/android/toga_android/widgets/slider.py
--- a/src/android/toga_android/widgets/slider.py
+++ b/src/android/toga_android/widgets/slider.py
@@ -35,26 +35,37 @@ def create(self):
 
     def get_value(self):
         minimum, maximum = self.interface.range
-        n_steps = self.interface.tick_count
-        if n_steps is None:
-            n_steps = DEFAULT_NUMBER_OF_TICKS
-        return (self.native.getProgress() * (maximum - minimum) / n_steps) + minimum
+        if self.interface.tick_count is not None and self.interface.tick_count <= 1:
+            return minimum
+        toga_tick_count = self.interface.tick_count or DEFAULT_NUMBER_OF_TICKS
+        android_slider_max = toga_tick_count - 1
+        tick_factor = (maximum - minimum) / android_slider_max
+        progress_scaled = self.native.getProgress() * tick_factor
+        result = progress_scaled + minimum
+        return result
 
     def set_value(self, value):
         minimum, maximum = self.interface.range
-        n_steps = self.interface.tick_count
-        if n_steps is None:
-            n_steps = DEFAULT_NUMBER_OF_TICKS
-        self.native.setProgress(int((maximum - value - minimum) * n_steps))
+        if self.interface.tick_count is not None and self.interface.tick_count <= 1:
+            android_progress = 0
+        else:
+            toga_tick_count = self.interface.tick_count or DEFAULT_NUMBER_OF_TICKS
+            android_slider_max = toga_tick_count - 1
+            tick_factor = (maximum - minimum) / android_slider_max
+            android_progress = int((value - minimum) * tick_factor)
+        self.native.setProgress(android_progress)
 
     def set_range(self, range):
         pass
 
     def set_tick_count(self, tick_count):
-        if tick_count is None:
-            self.native.setMax(DEFAULT_NUMBER_OF_TICKS)
+        # Since the Android slider slides from 0 to max inclusive, always subtract 1 from tick_count.
+        if self.interface.tick_count is None:
+            android_slider_max = DEFAULT_NUMBER_OF_TICKS - 1
         else:
-            self.native.setMax(int(tick_count) - 1)
+            android_slider_max = int(self.interface.tick_count - 1)
+        # Set the Android SeekBar max, clamping so it's non-negative.
+        self.native.setMax(max(0, android_slider_max))
 
     def rehint(self):
         self.native.measure(


Test patch: