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

Android background_color Transparency Fixes #3118

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
4 changes: 3 additions & 1 deletion android/src/toga_android/colors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from android.graphics import Color
from travertino.colors import TRANSPARENT

CACHE = {TRANSPARENT: Color.TRANSPARENT}
CACHE = {
TRANSPARENT: Color.TRANSPARENT,
}


def native_color(c):
Expand Down
6 changes: 3 additions & 3 deletions android/src/toga_android/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ def refreshed(self):
self.native_content.setLayoutParams(lp)

def add_content(self, widget):
self.native_content.addView(widget.native)
self.native_content.addView(widget.native_toplevel)

def remove_content(self, widget):
self.native_content.removeView(widget.native)
self.native_content.removeView(widget.native_toplevel)

def set_content_bounds(self, widget, x, y, width, height):
lp = RelativeLayout.LayoutParams(width, height)
lp.topMargin = y
lp.leftMargin = x
widget.native.setLayoutParams(lp)
widget.native_toplevel.setLayoutParams(lp)
93 changes: 63 additions & 30 deletions android/src/toga_android/widgets/base.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
from abc import ABC, abstractmethod
from decimal import ROUND_HALF_EVEN, Decimal

from android.graphics import PorterDuff, PorterDuffColorFilter, Rect
from android.graphics.drawable import ColorDrawable, InsetDrawable
from android.graphics import Color, PorterDuff, PorterDuffColorFilter
from android.graphics.drawable import ColorDrawable
from android.view import Gravity, View
from android.widget import RelativeLayout
from org.beeware.android import MainActivity
from travertino.size import at_least

from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT, TRANSPARENT

from ..colors import native_color
from toga.constants import CENTER, JUSTIFY, LEFT, RIGHT
from toga_android.colors import native_color


class Scalable:
Expand Down Expand Up @@ -54,6 +53,8 @@ def __init__(self, interface):
self.init_scale(self._native_activity)
self.create()

self.native_toplevel = self.native

# Some widgets, e.g. TextView, may throw an exception if we call measure()
# before setting LayoutParams.
self.native.setLayoutParams(
Expand All @@ -63,6 +64,7 @@ def __init__(self, interface):
)
)

self._default_background_color = Color.TRANSPARENT
# Immediately re-apply styles. Some widgets may defer style application until
# they have been added to a container.
self.interface.style.reapply()
Expand Down Expand Up @@ -119,9 +121,9 @@ def set_bounds(self, x, y, width, height):

def set_hidden(self, hidden):
if hidden:
self.native.setVisibility(View.INVISIBLE)
self.native_toplevel.setVisibility(View.INVISIBLE)
else:
self.native.setVisibility(View.VISIBLE)
self.native_toplevel.setVisibility(View.VISIBLE)

def set_font(self, font):
# By default, font can't be changed
Expand All @@ -132,33 +134,20 @@ def set_font(self, font):
# appearance. So each widget must decide how to implement this method, possibly
# using one of the utility functions below.
def set_background_color(self, color):
pass
self.set_background_simple(color)

def set_background_simple(self, value):
if not hasattr(self, "_default_background"):
self._default_background = self.native.getBackground()
def set_background_simple(self, color):
self.native_toplevel.setBackground(
ColorDrawable(
self._default_background_color if color is None else native_color(color)
)
)

if value in (None, TRANSPARENT):
self.native.setBackground(self._default_background)
else:
background = ColorDrawable(native_color(value))
if isinstance(self._default_background, InsetDrawable):
outer_padding = Rect()
inner_padding = Rect()
self._default_background.getPadding(outer_padding)
self._default_background.getDrawable().getPadding(inner_padding)
insets = [
getattr(outer_padding, name) - getattr(inner_padding, name)
for name in ["left", "top", "right", "bottom"]
]
background = InsetDrawable(background, *insets)
self.native.setBackground(background)

def set_background_filter(self, value):
def set_background_filter(self, color):
self.native.getBackground().setColorFilter(
None
if value in (None, TRANSPARENT)
else PorterDuffColorFilter(native_color(value), PorterDuff.Mode.SRC_IN)
if color is None
else PorterDuffColorFilter(native_color(color), PorterDuff.Mode.SRC_IN)
)

def set_text_align(self, alignment):
Expand Down Expand Up @@ -198,3 +187,47 @@ def android_text_align(value):
CENTER: Gravity.CENTER_HORIZONTAL,
JUSTIFY: Gravity.LEFT,
}[value]


# Most of the Android Widget have different effects applied them which provide
# the native look and feel of Android. These widgets' background consists of
# Drawables like ColorDrawable, InsetDrawable and other animation Effect Drawables
# like RippleDrawable. Often when such Effect Drawables are used, they are stacked
# along with other Drawables in a LayerDrawable.
#
# LayerDrawable once created cannot be modified and attempting to modify it or
# creating a new LayerDrawable using the elements of the original LayerDrawable
# stack, will destroy the native look and feel of the widgets. The original
# LayerDrawable cannot also be properly cloned. Using `getConstantState()` on the
# Drawable will produce an erroneous version of the original Drawable.
#
# These widgets also draw some of their background effects on their native parent.
# But in the Widget base class, the native parent is actually the root Box of the
# layout, and the parent Box is stacked under its children without any native
# parent/child relationship. So if the parent Box has a background color, it may
# conceal some of the background elements of its children.
#
# Hence, the best option to preserve the native look and feel of the these widgets is
# to contain them in a `RelativeLayout` and set the background color to the layout
# instead of the widget itself.
#
# ContainedWidget should act as a drop-in replacement against the Widget class for
# such widgets, without requiring the widgets to do anything extra on their part.
# It should be used for widgets that have an animated Effect Drawable as background
# and their native look and feel needs to be preserved.
class ContainedWidget(Widget):
def __init__(self, interface):
super().__init__(interface)

self.native_toplevel = RelativeLayout(self._native_activity)
self.native_toplevel.addView(self.native)

self.native.setLayoutParams(
RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT,
RelativeLayout.LayoutParams.MATCH_PARENT,
)
)
# Immediately re-apply styles. Some widgets may defer style application until
# they have been added to a container.
self.interface.style.reapply()
6 changes: 4 additions & 2 deletions android/src/toga_android/widgets/box.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from android.widget import RelativeLayout

from toga.colors import TRANSPARENT

from .base import Widget


class Box(Widget):
def create(self):
self.native = RelativeLayout(self._native_activity)

def set_background_color(self, value):
self.set_background_simple(value)
def set_background_color(self, color):
self.set_background_simple(TRANSPARENT if color is None else color)
6 changes: 4 additions & 2 deletions android/src/toga_android/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from java import dynamic_proxy
from travertino.size import at_least

from toga.colors import TRANSPARENT

from .label import TextViewWidget


Expand Down Expand Up @@ -49,8 +51,8 @@ def set_icon(self, icon):
def set_enabled(self, value):
self.native.setEnabled(value)

def set_background_color(self, value):
self.set_background_filter(value)
def set_background_color(self, color):
self.set_background_filter(None if color is TRANSPARENT else color)

def rehint(self):
if self._icon:
Expand Down
7 changes: 3 additions & 4 deletions android/src/toga_android/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -243,13 +243,12 @@ def get_image_data(self):
)
canvas = A_Canvas(bitmap)
background = self.native.getBackground()
if background:
background.draw(canvas)
background.draw(canvas)
self.native.draw(canvas)

stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
return bytes(stream.toByteArray())

def set_background_color(self, value):
self.set_background_simple(value)
def set_background_color(self, color):
self.set_background_simple(color)
4 changes: 3 additions & 1 deletion android/src/toga_android/widgets/dateinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from android.app import DatePickerDialog
from java import dynamic_proxy

from toga_android.widgets.base import ContainedWidget

from .internal.pickers import PickerBase


Expand All @@ -27,7 +29,7 @@ def onDateSet(self, view, year, month_0, day):
self.impl.interface.value = date(year, month_0 + 1, day)


class DateInput(PickerBase):
class DateInput(PickerBase, ContainedWidget):
@classmethod
def _get_icon(cls):
return R.drawable.ic_menu_my_calendar
Expand Down
3 changes: 2 additions & 1 deletion android/src/toga_android/widgets/divider.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ def create(self):
self._direction = self.interface.HORIZONTAL

def set_background_color(self, value):
self.set_background_simple(value)
# Do nothing, since background color of Divider shouldn't be changed.
pass

def get_direction(self):
return self._direction
Expand Down
4 changes: 2 additions & 2 deletions android/src/toga_android/widgets/imageview.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ def create(self):
self.native = A_ImageView(self._native_activity)
self.native.setAdjustViewBounds(True)

def set_background_color(self, value):
self.set_background_simple(value)
def set_background_color(self, color):
self.set_background_simple(color)

def set_image(self, image):
if image:
Expand Down
10 changes: 4 additions & 6 deletions android/src/toga_android/widgets/label.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from android.widget import TextView
from travertino.size import at_least

from toga.colors import TRANSPARENT
from toga.constants import JUSTIFY
from toga_android.colors import native_color

Expand All @@ -29,12 +30,6 @@ def set_font(self, font):
self.native, font._impl, self._default_typeface, self._default_text_size
)

def set_background_color(self, value):
# In the case of EditText, this causes any custom color to hide the bottom
# border line, but it's better than set_background_filter, which affects *only*
# the bottom border line.
self.set_background_simple(value)

def set_color(self, value):
if value is None:
self.native.setTextColor(self._default_text_color)
Expand Down Expand Up @@ -82,3 +77,6 @@ def rehint(self):

def set_text_align(self, value):
self.set_textview_alignment(value, Gravity.TOP)

def set_background_color(self, color):
self.set_background_simple(TRANSPARENT if color is None else color)
4 changes: 2 additions & 2 deletions android/src/toga_android/widgets/scrollcontainer.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,5 @@ def set_position(self, horizontal_position, vertical_position):
self.hScrollView.setScrollX(self.scale_in(horizontal_position))
self.vScrollView.setScrollY(self.scale_in(vertical_position))

def set_background_color(self, value):
self.set_background_simple(value)
def set_background_color(self, color):
self.set_background_simple(color)
4 changes: 2 additions & 2 deletions android/src/toga_android/widgets/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from android.widget import AdapterView, ArrayAdapter, Spinner
from java import dynamic_proxy

from .base import Widget
from toga_android.widgets.base import ContainedWidget


class TogaOnItemSelectedListener(dynamic_proxy(AdapterView.OnItemSelectedListener)):
Expand All @@ -20,7 +20,7 @@ def onNothingSelected(self, parent):
self.impl.on_change(None)


class Selection(Widget):
class Selection(ContainedWidget):
focusable = False

def create(self):
Expand Down
9 changes: 6 additions & 3 deletions android/src/toga_android/widgets/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
from android.widget import SeekBar
from java import dynamic_proxy

from toga.colors import TRANSPARENT
from toga.widgets.slider import IntSliderImpl

from .base import Widget
from toga_android.widgets.base import ContainedWidget

# Implementation notes
# ====================
Expand All @@ -31,7 +31,7 @@ def onStopTrackingTouch(self, native_seekbar):
self.impl.interface.on_release()


class Slider(Widget, IntSliderImpl):
class Slider(ContainedWidget, IntSliderImpl):
focusable = False
TICK_DRAWABLE = None

Expand Down Expand Up @@ -71,3 +71,6 @@ def rehint(self):
self.interface.intrinsic.height = self.scale_out(
self.native.getMeasuredHeight(), ROUND_UP
)

def set_background_color(self, color):
self.set_background_simple(TRANSPARENT if color is None else color)
4 changes: 3 additions & 1 deletion android/src/toga_android/widgets/switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from java import dynamic_proxy
from travertino.size import at_least

from toga_android.widgets.base import ContainedWidget

from .label import TextViewWidget


Expand All @@ -17,7 +19,7 @@ def onCheckedChanged(self, _button, _checked):
self._impl.interface.on_change()


class Switch(TextViewWidget):
class Switch(TextViewWidget, ContainedWidget):
focusable = False

def create(self):
Expand Down
4 changes: 2 additions & 2 deletions android/src/toga_android/widgets/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ def insert_column(self, index, heading, accessor):
def remove_column(self, index):
self.change_source(self.interface.data)

def set_background_color(self, value):
self.set_background_simple(value)
def set_background_color(self, color):
self.set_background_simple(color)

def set_font(self, font):
self._font_impl = font._impl
Expand Down
3 changes: 2 additions & 1 deletion android/src/toga_android/widgets/textinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from java import dynamic_proxy

from toga_android.keys import toga_key
from toga_android.widgets.base import ContainedWidget

from .label import TextViewWidget

Expand Down Expand Up @@ -55,7 +56,7 @@ def onFocusChange(self, view, has_focus):
self.impl._on_lose_focus()


class TextInput(TextViewWidget):
class TextInput(ContainedWidget, TextViewWidget):
def create(self, input_type=InputType.TYPE_CLASS_TEXT):
self.native = EditText(self._native_activity)
self.native.setInputType(input_type)
Expand Down
Loading