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: update toolbar after rotation #1552

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
50 changes: 48 additions & 2 deletions src/android/toga_android/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
from toga.command import Group

from .libs.activity import IPythonApp, MainActivity
from .libs.android import R__id
from .libs.android.view import Menu, MenuItem
from .libs.android.graphics import Drawable
from .libs.android.widget import FrameLayout, LinearLayout, LinearLayout__LayoutParams
from .libs.android.view import ViewGroup, Window as A_Window
from .libs.androidx.appcompat import Toolbar

from .window import Window


Expand Down Expand Up @@ -67,7 +72,8 @@ def onActivityResult(self, requestCode, resultCode, resultData):
print("No intent matching request code {requestCode}")

def onConfigurationChanged(self, new_config):
pass
if self._impl.toolbar:
self._impl.refresh_toolbar()

def onOptionsItemSelected(self, menuitem):
consumed = False
Expand Down Expand Up @@ -161,8 +167,8 @@ def __init__(self, interface):
self.interface = interface
self.interface._impl = self
self._listener = None

self.loop = android_events.AndroidEventLoop()
self.toolbar = None

@property
def native(self):
Expand All @@ -172,9 +178,49 @@ def create(self):
# The `_listener` listens for activity event callbacks. For simplicity,
# the app's `.native` is the listener's native Java class.
self._listener = TogaApp(self)

# Call findViewById on the Window rather than the Activity, to avoid triggering
# creation of the default toolbar.
self.content_parent = ViewGroup.__cast__(
self.native.getWindow().findViewById(R__id.content)
)
if self.content_parent.getChildCount():
# The default toolbar has already been created by the call to setContentView in old
# versions of the template, and there's no way to remove it.
pass
else:
# The default toolbar doesn't respond to configuration changes, so disable it
# and create our own.
self.native.supportRequestWindowFeature(A_Window.FEATURE_NO_TITLE)
self.linear_layout = LinearLayout(self.native)
self.linear_layout.setOrientation(LinearLayout.VERTICAL)
self.native.setContentView(self.linear_layout)
self.refresh_toolbar()
self.content_parent = FrameLayout(self.native)
self.linear_layout.addView(
self.content_parent,
LinearLayout__LayoutParams.MATCH_PARENT,
LinearLayout__LayoutParams.MATCH_PARENT
)

# Call user code to populate the main window
self.interface.startup()

# See https://developer.android.com/training/appbar/setting-up#add-toolbar
def refresh_toolbar(self):
if self.toolbar:
self.linear_layout.removeView(self.toolbar)
self.toolbar = Toolbar(self.native)
self.linear_layout.addView(
self.toolbar,
0,
LinearLayout__LayoutParams(
LinearLayout__LayoutParams.MATCH_PARENT,
LinearLayout__LayoutParams.WRAP_CONTENT
)
)
self.native.setSupportActionBar(self.toolbar)

def open_document(self, fileURL):
print("Can't open document %s (yet)" % fileURL)

Expand Down
2 changes: 2 additions & 0 deletions src/android/toga_android/libs/android/view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
MotionEvent = JavaClass("android/view/MotionEvent")
SubMenu = JavaClass("android/view/SubMenu")
View = JavaClass("android/view/View")
ViewGroup = JavaClass("android/view/ViewGroup")
ViewGroup__LayoutParams = JavaClass("android/view/ViewGroup$LayoutParams")
View__MeasureSpec = JavaClass("android/view/View$MeasureSpec")
View__OnTouchListener = JavaInterface("android/view/View$OnTouchListener")
ViewTreeObserver__OnGlobalLayoutListener = JavaInterface(
"android/view/ViewTreeObserver$OnGlobalLayoutListener"
)
Window = JavaClass("android/view/Window")
1 change: 1 addition & 0 deletions src/android/toga_android/libs/android/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
DatePickerDialog = JavaClass("android/app/DatePickerDialog")
DatePickerDialog__OnDateSetListener = JavaInterface("android/app/DatePickerDialog$OnDateSetListener")
EditText = JavaClass("android/widget/EditText")
FrameLayout = JavaClass("android/widget/FrameLayout")
HorizontalScrollView = JavaClass("android/widget/HorizontalScrollView")
ImageView = JavaClass("android/widget/ImageView")
ImageView__ScaleType = JavaClass("android/widget/ImageView$ScaleType")
Expand Down
4 changes: 4 additions & 0 deletions src/android/toga_android/libs/androidx/appcompat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from rubicon.java import JavaClass


Toolbar = JavaClass("androidx/appcompat/widget/Toolbar")
25 changes: 12 additions & 13 deletions src/android/toga_android/window.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
from .libs.android import R__id
from .libs.android.view import ViewTreeObserver__OnGlobalLayoutListener


class AndroidViewport:
# `content_parent` should be the view that will become the parent of the widget passed to
# `Window.set_content`. This ensures that the viewport `width` and `height` attributes
# return the usable area of the app, not including the action bar or status bar.
def __init__(self, content_parent):
self.content_parent = content_parent
self.dpi = content_parent.getContext().getResources().getDisplayMetrics().densityDpi
def __init__(self, app):
self.app = app
self.dpi = app.native.getResources().getDisplayMetrics().densityDpi
# Toga needs to know how the current DPI compares to the platform default,
# which is 160: https://developer.android.com/training/multiscreen/screendensities
self.baseline_dpi = 160
self.scale = float(self.dpi) / self.baseline_dpi

@property
def width(self):
return self.content_parent.getWidth()
return self.app.content_parent.getWidth()

@property
def height(self):
return self.content_parent.getHeight()
return self.app.content_parent.getHeight()


class Window(ViewTreeObserver__OnGlobalLayoutListener):
Expand All @@ -34,9 +33,8 @@ def __init__(self, interface, title, position, size):

def set_app(self, app):
self.app = app
content_parent = self.app.native.findViewById(R__id.content).__global__()
self.viewport = AndroidViewport(content_parent)
content_parent.getViewTreeObserver().addOnGlobalLayoutListener(self)
self.viewport = AndroidViewport(app)
app.content_parent.getViewTreeObserver().addOnGlobalLayoutListener(self)

def onGlobalLayout(self):
"""This listener is run after each native layout pass. If any view's size or position has
Expand All @@ -51,10 +49,11 @@ def onGlobalLayout(self):
def set_content(self, widget):
# Set the widget's viewport to be based on the window's content.
widget.viewport = self.viewport
# Set the app's entire contentView to the desired widget. This means that
# calling Window.set_content() on any Window object automatically updates
# the app, meaning that every Window object acts as the MainWindow.
self.app.native.setContentView(widget.native)

# Set the activity's entire content to the desired widget. This means that
# every Window object currently acts as the MainWindow.
self.app.content_parent.removeAllViews()
self.app.content_parent.addView(widget.native)

# Attach child widgets to widget as their container.
for child in widget.interface.children:
Expand Down