Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Android font support #1005

Merged
merged 5 commits into from
Jul 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/android/toga_android/factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from .app import App, MainWindow
from .fonts import Font
from .icons import Icon
from .images import Image
from .paths import paths
Expand All @@ -24,6 +25,7 @@ def not_implemented(feature):
"App",
"Box",
"Button",
"Font",
"Icon",
"Image",
"ImageView",
Expand Down
60 changes: 60 additions & 0 deletions src/android/toga_android/fonts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from toga.fonts import (
BOLD,
CURSIVE,
FANTASY,
ITALIC,
MONOSPACE,
SANS_SERIF,
SERIF,
SYSTEM,
SYSTEM_DEFAULT_FONT_SIZE,
)

from .libs.android_widgets import (
Typeface,
)


class Font:
def __init__(self, interface):
self.interface = interface

def get_size(self):
# Default system font size on Android is 14sp (sp = dp, but is separately
# scalable in user settings). For what it's worth, Toga's default is 12pt.
if self.interface.size == SYSTEM_DEFAULT_FONT_SIZE:
font_size = 14
else:
font_size = self.interface.size
return float(font_size)

def get_style(self):
if self.interface.weight == BOLD:
if self.interface.style == ITALIC:
return Typeface.BOLD_ITALIC
else:
return Typeface.BOLD
if self.interface.style == ITALIC:
return Typeface.ITALIC
return Typeface.NORMAL

def get_typeface(self):
if self.interface.family is SYSTEM:
family = Typeface.DEFAULT
elif self.interface.family is SERIF:
family = Typeface.SERIF
elif self.interface.family is SANS_SERIF:
family = Typeface.SANS_SERIF
elif self.interface.family is MONOSPACE:
family = Typeface.MONOSPACE
elif self.interface.family is CURSIVE:
family = Typeface.create("cursive", Typeface.NORMAL)
elif self.interface.family is FANTASY:
# Android appears to not have a fantasy font available by default,
# but if it ever does, we'll start using it. Android seems to choose
# a serif font when asked for a fantasy font.
family = Typeface.create("fantasy", Typeface.NORMAL)
else:
family = Typeface.create(self.interface.family, Typeface.NORMAL)

return family
2 changes: 2 additions & 0 deletions src/android/toga_android/libs/android_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
Spinner = JavaClass("android/widget/Spinner")
TextView = JavaClass("android/widget/TextView")
TextWatcher = JavaInterface("android/text/TextWatcher")
TypedValue = JavaClass("android/util/TypedValue")
Typeface = JavaClass("android/graphics/Typeface")
ViewGroup__LayoutParams = JavaClass("android/view/ViewGroup$LayoutParams")
View__MeasureSpec = JavaClass("android/view/View$MeasureSpec")

Expand Down
9 changes: 9 additions & 0 deletions src/android/toga_android/widgets/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ def __init__(self, interface):
# can pass it as `context` when creating native Android widgets.
self._native_activity = MainActivity.singletonThis
self.create()
# Immediately re-apply styles. Some widgets may defer style application until
# they have been added to a container.
self.interface.style.reapply()

def set_app(self, app):
pass
Expand Down Expand Up @@ -62,6 +65,12 @@ def set_background_color(self, color):
# By default, background color can't be changed.
pass

def set_alignment(self, alignment):
pass # If appropriate, a widget subclass will implement this.

def set_color(self, color):
pass # If appropriate, a widget subclass will implement this.

# INTERFACE

def add_child(self, child):
Expand Down
3 changes: 3 additions & 0 deletions src/android/toga_android/widgets/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ def create(self):
self.native = RelativeLayout(MainActivity.singletonThis)

def set_child_bounds(self, widget, x, y, width, height):
# Avoid setting child boundaries if `create()` has not been called.
if not widget.native:
return
# We assume `widget.native` has already been added to this `RelativeLayout`.
#
# We use `topMargin` and `leftMargin` to perform absolute layout. Not very
Expand Down
20 changes: 19 additions & 1 deletion src/android/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from travertino.size import at_least

from ..libs.android_widgets import Gravity, TextView, View__MeasureSpec
from ..libs.android_widgets import Gravity, TextView, TypedValue, View__MeasureSpec
from .base import Widget, align


Expand All @@ -12,7 +12,17 @@ def create(self):
def set_text(self, value):
self.native.setText(value)

def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

def rehint(self):
# Refuse to rehint an Android TextView if it has no LayoutParams yet.
# Calling measure() on an Android TextView w/o LayoutParams raises NullPointerException.
if self.native.getLayoutParams() is None:
return
# Ask the Android TextView first for the height it would use in its
# wildest dreams. This is the height of one line of text.
self.native.measure(
Expand All @@ -30,4 +40,12 @@ def rehint(self):
self.interface.intrinsic.width = at_least(self.native.getMeasuredWidth())

def set_alignment(self, value):
# Refuse to set alignment if create() has not been called.
if self.native is None:
return
# Refuse to set alignment if widget has no container.
# On Android, calling setGravity() when the widget has no LayoutParams
# results in a NullPointerException.
if self.native.getLayoutParams() is None:
return
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))
8 changes: 6 additions & 2 deletions src/android/toga_android/widgets/multilinetextinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
EditText,
Gravity,
InputType,
TypedValue,
)
from .base import Widget, align

Expand All @@ -32,8 +33,11 @@ def set_placeholder(self, value):
def set_alignment(self, value):
self.native.setGravity(Gravity.TOP | align(value))

def set_font(self, value):
self.interface.factory.not_implemented("MutlineTextInput.set_font()")
def set_font(self, font):
if font:
font_impl = font.bind(self.interface.factory)
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

def set_value(self, value):
self.native.setText(value)
Expand Down
6 changes: 5 additions & 1 deletion src/android/toga_android/widgets/numberinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Gravity,
InputType,
TextWatcher,
TypedValue,
View__MeasureSpec
)
from .base import Widget, align
Expand Down Expand Up @@ -71,7 +72,10 @@ def set_alignment(self, value):
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))

def set_font(self, font):
self.interface.factory.not_implemented("NumberInput.set_font()")
if font:
font_impl = font.bind(self.interface.factory)
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

def set_value(self, value):
# Store a string in the Android widget. The `afterTextChanged` method
Expand Down
18 changes: 16 additions & 2 deletions src/android/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
EditText,
Gravity,
TextWatcher,
View__MeasureSpec
TypedValue,
View__MeasureSpec,
)
from .base import Widget, align

Expand Down Expand Up @@ -42,10 +43,18 @@ def set_placeholder(self, value):
self.native.setHint(value if value is not None else "")

def set_alignment(self, value):
# Refuse to set alignment unless widget has been added to a container.
# This is because Android EditText requires LayoutParams before
# setGravity() can be called.
if self.native.getLayoutParams() is None:
return
self.native.setGravity(Gravity.CENTER_VERTICAL | align(value))

def set_font(self, font):
self.interface.factory.not_implemented("TextInput.set_font()")
if font:
font_impl = font.bind(self.interface.factory)
self.native.setTextSize(TypedValue.COMPLEX_UNIT_SP, font_impl.get_size())
self.native.setTypeface(font_impl.get_typeface(), font_impl.get_style())

def set_value(self, value):
self.native.setText(value)
Expand All @@ -56,6 +65,11 @@ def set_on_change(self, handler):

def rehint(self):
self.interface.intrinsic.width = at_least(self.interface.MIN_WIDTH)
# Refuse to call measure() if widget has no container, i.e., has no LayoutParams.
# On Android, EditText's measure() throws NullPointerException if the widget has no
# LayoutParams.
if self.native.getLayoutParams() is None:
return
self.native.measure(
View__MeasureSpec.UNSPECIFIED, View__MeasureSpec.UNSPECIFIED
)
Expand Down