Rolling number

v0.0.10

v0.0.10StableMobStatelessApache-2.0
Installation mix mishka.ui.gen.mob rolling_number
Source View Source

The Mishka Chelekom Mob Rolling Number shows an integer on iOS and Android with a thousands separator. Your screen counts it from one value to another by walking the eased steps that the component computes. It is the native port of the headless Rolling Number, built for Mob, which drives Jetpack Compose and SwiftUI from Elixir.

Overview of the Mob Rolling Number component for iOS and Android

On the web a rolling number runs a JavaScript hook that tweens from the old value to the new one. A Mob component is a pure function of its props and cannot animate itself, and a component that started a timer would start a new one on every render. So MishkaRollingNumber renders whatever integer it is given, formatted, and the screen does the rolling.

MishkaRollingNumber.steps/3 returns the eased sequence of values between two numbers. The screen walks the sequence with Process.send_after/3 and assigns each value in turn. The timer stays in your screen, and the easing is a pure function you can test.

elixir
def handle_info({:roll, [next | rest]}, socket) do
  if rest != [], do: Process.send_after(self(), {:roll, rest}, 16)
  {:noreply, Mob.Socket.assign(socket, :count, next)}
end

The number is one Text node, so it takes the same size and color tokens as any other text. separator groups the thousands.

Generate the rolling number component

Run this from your Mob app's root to add the component to lib/<app>/components/:

bash
mix mishka.ui.gen.mob rolling_number

The generator registers the <RollingNumber> tag so you can use it in ~MOB markup. The examples on this page use the mishka_ prefix of the showcase app (--module-prefix mishka_), so the tag is <MishkaRollingNumber>. Without a prefix the tag is <RollingNumber>.

Rolling number counting up

The component renders whatever number it is given. steps/3 produces the eased sequence, and the screen walks it. MishkaRollingNumber.steps(socket.assigns.count, 1_284, 18) returns 18 values from the current count to 1,284. The screen assigns the first value and sends the rest back to itself every 24 milliseconds. The timer message is {:tap, {:step, rest}}, so it reaches the same clauses as a button tap. The code below uses the showcase app's handle/2 callback. In your own screen, write the clauses as handle_info/2.

Rolling number counting up

Counting up card with the number 0 centered above a full width dark Roll to 1,284 button and a white Reset button under it, in the MishkaMob showcase app on Android
A large number reading 1,284 above Roll to 1,284 and Reset buttons.

Left: the example as it loads. Right: after a tap. The number counts up to its target.

elixir
<MishkaRollingNumber value={@count} />

# kick it off
def handle(:roll, socket) do
  [next | rest] = MishkaRollingNumber.steps(socket.assigns.count, 1_284, 18)
  if rest != [], do: Process.send_after(self(), {:tap, {:step, rest}}, 24)
  Mob.Socket.assign(socket, :count, next)
end

# and walk it
def handle({:step, [next | rest]}, socket) do
  if rest != [], do: Process.send_after(self(), {:tap, {:step, rest}}, 24)
  Mob.Socket.assign(socket, :count, next)
end

Rolling number counting down

steps/3 counts down as well as up, and the last value lands exactly on the target. Here steps(500, 10, 7) ends on 10. The card in the screenshot has a button that rolls the number on the first card down to 42.

Rolling number counting down

Counting down card with one full width white button labeled Roll down to 42, which rolls the number in the card above, in the MishkaMob showcase app on Android
elixir
MishkaRollingNumber.steps(500, 10, 7) |> List.last()
#=> 10

Grouping thousands

separator groups the thousands. The default is a comma, a space gives the European style, and an empty string turns grouping off. Negative numbers keep their sign.

Grouping thousands

Grouping card with four stacked numbers: 1,234,567 with commas, 1 234 567 with spaces, 1234567 with no grouping and -98,765 in red, in the MishkaMob showcase app on Android
heex
<MishkaRollingNumber value={1234567} />
<MishkaRollingNumber value={1234567} separator=" " />
<MishkaRollingNumber value={1234567} separator="" />
<MishkaRollingNumber value={-98765} color={0xFFDC2626} />

Rolling number size and color

The number is one Text node, so text_size and color take the same tokens as any other text. The card in the screenshot also shows a smaller, muted 1,234 below the large one.

Rolling number size and color

Size and colour card with the number 1,234 in very large near black type and a small muted 1,234 below it, in the MishkaMob showcase app on Android
heex
<MishkaRollingNumber value={1234} text_size={:"4xl"} color={:primary} />

Props

PropTypeDefaultDescription
valueinteger0The number shown.
separatorstring","Thousands separator. An empty string turns grouping off.
text_sizesize token:"2xl"Number size.
colorcolor token or ARGB integer:on_surfaceNumber color.

Helper functions

FunctionDescription
steps(from, to, count)Returns count eased values from from toward to, for the screen to walk on a timer. The curve is an ease-out cubic, fast at first and settling on the target, and the last value is to. steps(0, 10, 5) returns [5, 8, 9, 10, 10]. A count of 0 or less returns [to].
format(value, separator)Groups the thousands of an integer from the right. The separator defaults to a comma. format(1234567, ",") returns "1,234,567", and format(1234, "") returns "1234".

Differences from the web component

These attributes of the web component are not ported:

  • duration, because the screen's timer sets the pace, as the overview shows.
  • locale, because Elixir has no Intl.NumberFormat and guessing the separators of a locale would be worse than not trying. Pass the character you want in separator.
  • id and the *_class attributes.