From 6c229f3a25ca6548206cc9a2facbaf91e6c7283c Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Wed, 17 Oct 2018 19:09:56 -0700 Subject: [PATCH 1/5] [TIMOB-26246] Android: Added Ti.UI.Window "extendSafeArea", "safeAreaPadding", and inset/notch support - [TIMOB-26246] Added handling of Android P inset/notch/cutout. * Added Ti.UI.Window property "extendSafeArea" to render content beneath insets. - [TIMOB-26427] Added Ti.UI.Window property "safeAreaPadding". * Applies to Android 4.4 and newer OS versions when "extendSafeArea" is true. * Needed to detect size of insets for translucent StatusBar/NavBar and Android P notches. - [TIMOB-26459] Added constants for Ti.UI.Window property "windowFlags". * Ti.UI.Android.FLAG_TRANSLUCENT_NAVIGATION * Ti.UI.Android.FLAG_TRANSLUCENT_STATUS - [TIMOB-25810] Fixed bug where ActionBar height won't resize upon orientation changes. - [TIMOB-26460] Fixed bug where Toolbar height set to Ti.UI.SIZE won't resize upon orientation change. - [TIMOB-26442] Fixed bug where Toolbar with "extendBackground" to true is too tall if both status bar and nav bar are translucent. - Modified DrawerLayout XML to not fit its "center" view under top insets. * The XML setting was wrong. It was intended to place left/right drawers under top inset, but it wasn't doing that. * Better solution is to allow left/center/right layouts to overlap insets and use a Toolbar with "extendBackground" true to position itself beneath inset. --- .../res/layout/titanium_ui_drawer_layout.xml | 3 +- .../ti/modules/titanium/ui/TabGroupProxy.java | 32 ++ .../ti/modules/titanium/ui/WindowProxy.java | 4 + .../titanium/ui/android/AndroidModule.java | 5 + .../modules/titanium/ui/widget/TiToolbar.java | 76 +++- .../appcelerator/titanium/TiBaseActivity.java | 137 +++++- .../java/org/appcelerator/titanium/TiC.java | 5 + .../titanium/proxy/TiWindowProxy.java | 109 +++++ .../view/TiActionBarStyleHandler.java | 108 +++++ .../view/TiActivitySafeAreaMonitor.java | 390 ++++++++++++++++++ .../titanium/view/TiCompositeLayout.java | 94 +++++ .../titanium/view/TiToolbarStyleHandler.java | 114 +++++ apidoc/Titanium/UI/Android/Android.yml | 26 ++ apidoc/Titanium/UI/Window.yml | 77 +++- tests/Resources/ti.ui.window.addontest.js | 68 +++ 15 files changed, 1210 insertions(+), 38 deletions(-) create mode 100644 android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java create mode 100644 android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java create mode 100644 android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java create mode 100644 tests/Resources/ti.ui.window.addontest.js diff --git a/android/modules/ui/res/layout/titanium_ui_drawer_layout.xml b/android/modules/ui/res/layout/titanium_ui_drawer_layout.xml index 7bb6dbaa819..7ee70be0d4c 100644 --- a/android/modules/ui/res/layout/titanium_ui_drawer_layout.xml +++ b/android/modules/ui/res/layout/titanium_ui_drawer_layout.xml @@ -17,8 +17,7 @@ android:layout_width="match_parent" android:minHeight="?attr/actionBarSize" android:background="?attr/colorPrimary" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light" - android:fitsSystemWindows="true"/> + app:popupTheme="@style/ThemeOverlay.AppCompat.Light"/> clonedTabs = null; + synchronized (this.tabs) + { + clonedTabs = (ArrayList) this.tabs.clone(); + } + if (clonedTabs == null) { + return; + } + + // Fire a safe-area change event for each tab window. + for (TabProxy tab : clonedTabs) { + if (tab != null) { + TiWindowProxy window = tab.getWindow(); + if (window != null) { + window.fireSafeAreaChangedEvent(); + } + } + } + } + @Override public String getApiName() { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java index 9461d3a9c13..bf49c6ad1ed 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/WindowProxy.java @@ -350,6 +350,10 @@ private void fillIntent(Activity activity, Intent intent) intent.putExtra(TiC.PROPERTY_WINDOW_PIXEL_FORMAT, TiConvert.toInt(getProperty(TiC.PROPERTY_WINDOW_PIXEL_FORMAT), PixelFormat.UNKNOWN)); } + if (hasProperty(TiC.PROPERTY_EXTEND_SAFE_AREA)) { + boolean value = TiConvert.toBoolean(getProperty(TiC.PROPERTY_EXTEND_SAFE_AREA), false); + intent.putExtra(TiC.PROPERTY_EXTEND_SAFE_AREA, value); + } // Set the theme property if (hasProperty(TiC.PROPERTY_THEME)) { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java b/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java index 7314ad8edfd..918d00f840c 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/android/AndroidModule.java @@ -33,6 +33,11 @@ public class AndroidModule extends KrollModule { private static final String TAG = "UIAndroidModule"; + @Kroll.constant + public static final int FLAG_TRANSLUCENT_NAVIGATION = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + @Kroll.constant + public static final int FLAG_TRANSLUCENT_STATUS = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; + @Kroll.constant public static final int PIXEL_FORMAT_A_8 = PixelFormat.A_8; @Kroll.constant diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java index 7cbd4edeab4..0185df07332 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java @@ -1,6 +1,8 @@ package ti.modules.titanium.ui.widget; +import android.content.res.Configuration; import android.graphics.Color; +import android.graphics.Rect; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -18,7 +20,9 @@ import org.appcelerator.titanium.TiDimension; import org.appcelerator.titanium.proxy.TiViewProxy; import org.appcelerator.titanium.util.TiColorHelper; +import org.appcelerator.titanium.view.TiCompositeLayout; import org.appcelerator.titanium.view.TiDrawableReference; +import org.appcelerator.titanium.view.TiToolbarStyleHandler; import org.appcelerator.titanium.view.TiUIView; public class TiToolbar extends TiUIView implements Handler.Callback @@ -66,7 +70,53 @@ public class TiToolbar extends TiUIView implements Handler.Callback public TiToolbar(TiViewProxy proxy) { super(proxy); - toolbar = new Toolbar(proxy.getActivity()); + toolbar = new Toolbar(proxy.getActivity()) { + @Override + protected void onConfigurationChanged(Configuration newConfig) + { + // If auto-sized, then resize toolbar height and font size to what's defined in XML. + // Note: Typically, the default height is 56dp in portrait and 48dp in landscape. + TiCompositeLayout.LayoutParams params = TiToolbar.this.getLayoutParams(); + boolean isAutoSized = (params != null) ? params.hasAutoSizedHeight() : true; + if (isAutoSized) { + TiToolbarStyleHandler styleHandler = new TiToolbarStyleHandler(this); + styleHandler.onConfigurationChanged(newConfig); + } + super.onConfigurationChanged(newConfig); + } + + @Override + protected boolean fitSystemWindows(Rect insets) + { + // Do custom inset handling if "extendBackground" was applied to toolbar. + if ((insets != null) && getFitsSystemWindows()) { + // Determine if we need to pad the top or bottom based on toolbar's y-axis position. + boolean isPaddingTop = true; + TiCompositeLayout.LayoutParams params = TiToolbar.this.getLayoutParams(); + if (params != null) { + if ((params.optionTop == null) && (params.optionCenterY == null)) { + if ((params.optionBottom != null) && (params.optionBottom.getAsPixels(this) <= 0)) { + // Toolbar is docked to the bottom of the view. So, pad the bottom instead. + isPaddingTop = false; + } + } + } + + // Create a new insets object with either the top or bottom inset padding stripped off. + // Note: We never want the toolbar to pad both the top and bottom. + // Especially when toolbar is docked to top of view but using a translucent navigation bar. + insets = new Rect(insets); + if (isPaddingTop) { + insets.bottom = 0; + } else { + insets.top = 0; + } + } + + // Apply the insets to the toolbar. (Google blindly pads view based on these insets.) + return super.fitSystemWindows(insets); + } + }; setNativeView(toolbar); } @@ -131,27 +181,15 @@ private void handleBackgroundExtended() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { Window window = TiApplication.getAppCurrentActivity().getWindow(); - //Compensate for status bar's height + // Compensate for status bar's height toolbar.setFitsSystemWindows(true); - //Set flags for the current window that allow drawing behind status bar - window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE - | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); - window.setStatusBarColor(Color.TRANSPARENT); - } - } - /** - * Calculates the Status Bar's height depending on the device - * @return The status bar's height. 0 if the API level does not have status_bar_height resource - */ - private int calculateStatusBarHeight() - { - int resourceId = - TiApplication.getAppCurrentActivity().getResources().getIdentifier("status_bar_height", "dimen", "android"); - if (resourceId > 0) { - return TiApplication.getAppCurrentActivity().getResources().getDimensionPixelSize(resourceId); + // Set flags for the current window that allow drawing behind status bar + int flags = window.getDecorView().getSystemUiVisibility(); + flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + window.getDecorView().setSystemUiVisibility(flags); + window.setStatusBarColor(Color.TRANSPARENT); } - return 0; } /** diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java index 96a3fe94803..84b755ecb56 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java @@ -1,6 +1,6 @@ /** * Appcelerator Titanium Mobile - * Copyright (c) 2009-2014 by Appcelerator, Inc. All Rights Reserved. + * Copyright (c) 2009-2018 by Axway, Inc. All Rights Reserved. * Licensed under the terms of the Apache Public License * Please see the LICENSE included with this distribution for details. */ @@ -40,6 +40,8 @@ import org.appcelerator.titanium.util.TiPlatformHelper; import org.appcelerator.titanium.util.TiUIHelper; import org.appcelerator.titanium.util.TiWeakList; +import org.appcelerator.titanium.view.TiActionBarStyleHandler; +import org.appcelerator.titanium.view.TiActivitySafeAreaMonitor; import org.appcelerator.titanium.view.TiCompositeLayout; import org.appcelerator.titanium.view.TiCompositeLayout.LayoutArrangement; @@ -53,6 +55,7 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.PixelFormat; +import android.graphics.Rect; import android.hardware.SensorManager; import android.os.Build; import android.os.Bundle; @@ -97,6 +100,8 @@ public abstract class TiBaseActivity extends AppCompatActivity implements TiActi new TiWeakList(); private APSAnalytics analytics = APSAnalytics.getInstance(); private boolean sustainMode = false; + private TiActionBarStyleHandler actionBarStyleHandler; + private TiActivitySafeAreaMonitor safeAreaMonitor; public static class PermissionContextData { @@ -530,10 +535,71 @@ protected void windowCreated(Bundle savedInstanceState) setFullscreen(fullscreen); + // Add additional window flags to better handle fullscreen support on devices with notches. + { + // Fetch flags. + int uiFlags = getWindow().getDecorView().getSystemUiVisibility(); + int allWindowFlags = windowFlags | getWindow().getAttributes().flags; + + // If status bar is to be hidden, then we must also set the translucent status bar flag + // or else devices with a notch will show a black bar where the status bar used to be. + boolean isHidingStatusBar = (allWindowFlags & WindowManager.LayoutParams.FLAG_FULLSCREEN) != 0; + isHidingStatusBar |= (uiFlags & View.SYSTEM_UI_FLAG_FULLSCREEN) != 0; + isHidingStatusBar |= (uiFlags & View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) != 0; + if (isHidingStatusBar) { + windowFlags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; + } + + // If navigation bar is to be hidden, then we must also set its translucent flag + // or else devices with a notch will show a black bar where the navigation bar used to be. + if ((uiFlags & View.SYSTEM_UI_FLAG_HIDE_NAVIGATION) != 0) { + windowFlags |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + } + } + + // Always allow screen cutouts/notches (such as the camera) to overlap window. + // Note: This won't overlap window's inner contents unless we call setFitsSystemWindows(true) down below, + // which is only enabled when Titanium's "extendSafeArea" property is set true. + if (Build.VERSION.SDK_INT >= 28) { + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(params); + } + + // Add the flags provided via property 'windowFlags'. if (windowFlags > 0) { getWindow().addFlags(windowFlags); } + // Remove translucent StatusBar/NavigationBar flags if window is not set up to extend beneath them. + // Not doing so will cause window to stretch beneath them anyways, but will fail to render there. + if (this.layout.getFitsSystemWindows()) { + int mask = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS; + mask |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; + if ((getWindow().getAttributes().flags & mask) != 0) { + String message = "You cannot use a translucent status bar or navigation bar unless you " + + "set the window's '" + TiC.PROPERTY_EXTEND_SAFE_AREA + "' property to true."; + Log.w(TAG, message); + getWindow().clearFlags(mask); + } + } + + // Update system UI flags with based on currently assigned translucency flags. + { + int systemUIFlags = 0; + int allWindowFlags = getWindow().getAttributes().flags; + if ((allWindowFlags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS) != 0) { + systemUIFlags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + } + if ((allWindowFlags & WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) != 0) { + systemUIFlags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + } + if (systemUIFlags != 0) { + systemUIFlags |= getWindow().getDecorView().getSystemUiVisibility(); + getWindow().getDecorView().setSystemUiVisibility(systemUIFlags); + } + } + if (modal) { if (Build.VERSION.SDK_INT < TiC.API_LEVEL_ICE_CREAM_SANDWICH) { // This flag is deprecated in API 14. On ICS, the background is not blurred but straight black. @@ -587,6 +653,7 @@ protected void onCreate(Bundle savedInstanceState) inForeground = true; TiApplication tiApp = getTiApp(); + this.safeAreaMonitor = new TiActivitySafeAreaMonitor(this); if (tiApp.isRestartPending()) { super.onCreate(savedInstanceState); @@ -637,11 +704,22 @@ protected void onCreate(Bundle savedInstanceState) // Doing this on every create in case the activity is externally created. TiPlatformHelper.getInstance().intializeDisplayMetrics(this); + // Create the root content layout, if not done already. if (layout == null) { layout = createLayout(); } - if (intent != null && intent.hasExtra(TiC.PROPERTY_KEEP_SCREEN_ON)) { - layout.setKeepScreenOn(intent.getBooleanExtra(TiC.PROPERTY_KEEP_SCREEN_ON, layout.getKeepScreenOn())); + + // Extend window's view under screen insets, if requested. + boolean extendSafeArea = false; + if (intent != null) { + extendSafeArea = intent.getBooleanExtra(TiC.PROPERTY_EXTEND_SAFE_AREA, false); + } + layout.setFitsSystemWindows(!extendSafeArea); + + // Enable/disable timer used to turn off the screen if idle. + if ((intent != null) && intent.hasExtra(TiC.PROPERTY_KEEP_SCREEN_ON)) { + boolean keepScreenOn = intent.getBooleanExtra(TiC.PROPERTY_KEEP_SCREEN_ON, layout.getKeepScreenOn()); + layout.setKeepScreenOn(keepScreenOn); } // Set the theme of the activity before calling super.onCreate(). @@ -671,6 +749,30 @@ protected void onCreate(Bundle savedInstanceState) } super.onCreate(savedInstanceState); + // If activity is using Google's default ActionBar, then the below will return an ActionBar style handler + // intended to be called by onConfigurationChanged() which will resize its title bar and font. + // Note: We need to do this since we override "configChanges" in the "AndroidManifest.xml". + // Default ActionBar height is typically 56dp for portrait and 48dp for landscape. + this.actionBarStyleHandler = TiActionBarStyleHandler.from(this); + + // If Google's ActionBar is being used, then add it to the top inset height. (Exclude from safe-area.) + // Note: If a toolbar is passed to AppCompatActivity.setSupportActionBar(), then ActionBar is a wrapper + // around that toolbar under our content view and is included in the safe-area. + this.safeAreaMonitor.setActionBarAddedAsInset(this.actionBarStyleHandler != null); + + // Start handling safe-area inset changes. + this.safeAreaMonitor.setOnChangedListener(new TiActivitySafeAreaMonitor.OnChangedListener() { + @Override + public void onChanged(TiActivitySafeAreaMonitor monitor) + { + TiWindowProxy windowProxy = TiBaseActivity.this.window; + if (windowProxy != null) { + windowProxy.fireSafeAreaChangedEvent(); + } + } + }); + this.safeAreaMonitor.start(); + try { windowCreated(savedInstanceState); } catch (Throwable t) { @@ -687,7 +789,7 @@ protected void onCreate(Bundle savedInstanceState) // If user changed the layout during app.js load, keep that if (!overridenLayout) { - setContentView(layout); + super.setContentView(layout); } // Set the title of the activity after setContentView. @@ -1111,6 +1213,13 @@ public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + // Update ActionBar height and font size, if needed. + // Handler will only be null if activity was set up without a title bar. + if (this.actionBarStyleHandler != null) { + this.actionBarStyleHandler.onConfigurationChanged(newConfig); + } + + // Notify all listener of this configuration change. for (WeakReference listener : configChangedListeners) { if (listener.get() != null) { listener.get().onConfigurationChanged(this, newConfig); @@ -1544,6 +1653,9 @@ protected void onDestroy() //Clean up dialogs when activity is destroyed. releaseDialogs(true); + // Stop listening for safe-area inset changes. + this.safeAreaMonitor.stop(); + if (tiApp.isRestartPending()) { super.onDestroy(); if (!isFinishing()) { @@ -1783,4 +1895,21 @@ public void setSustainMode(boolean sustainMode) Log.w(TAG, "sustainedPerformanceMode is not supported on this device"); } } + + /** + * Gets the safe area in pixels, relative to the root decor view. This is the region between + * the top/bottom/left/right insets that overlap the view's content such as a translucent + * status bar, translucent navigation bar, or screen notches. + * @return + * Returns the safe area region in pixels relative to the root decor view. + *

+ * Returns null if activity's root view is not available, such as after it's been destroyed. + */ + public Rect getSafeAreaRect() + { + if (this.safeAreaMonitor == null) { + return null; + } + return this.safeAreaMonitor.getSafeAreaRect(); + } } diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiC.java b/android/titanium/src/java/org/appcelerator/titanium/TiC.java index 1800cc86503..f22cec920d4 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiC.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiC.java @@ -1603,6 +1603,11 @@ public class TiC */ public static final String PROPERTY_EXTEND_BACKGROUND = "extendBackground"; + /** + * @module.api + */ + public static final String PROPERTY_EXTEND_SAFE_AREA = "extendSafeArea"; + /** * @module.api */ diff --git a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java index 8c49b1ea450..a355cacf4c8 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java +++ b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java @@ -21,6 +21,7 @@ import org.appcelerator.titanium.TiBaseActivity; import org.appcelerator.titanium.TiBlob; import org.appcelerator.titanium.TiC; +import org.appcelerator.titanium.TiDimension; import org.appcelerator.titanium.util.TiConvert; import org.appcelerator.titanium.util.TiDeviceOrientation; import org.appcelerator.titanium.util.TiUIHelper; @@ -31,6 +32,7 @@ import android.app.Activity; import android.app.ActivityOptions; import android.content.pm.ActivityInfo; +import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Message; @@ -39,6 +41,9 @@ import android.util.Pair; import android.view.Display; import android.view.View; +import android.view.ViewParent; +import android.view.Window; + // clang-format off @Kroll.proxy(propertyAccessors = { TiC.PROPERTY_EXIT_ON_CLOSE, @@ -57,6 +62,7 @@ public abstract class TiWindowProxy extends TiViewProxy private static final int MSG_FIRST_ID = TiViewProxy.MSG_LAST_ID + 1; private static final int MSG_OPEN = MSG_FIRST_ID + 100; private static final int MSG_CLOSE = MSG_FIRST_ID + 101; + private static final int MSG_GET_SAFE_AREA_PADDING = MSG_FIRST_ID + 102; protected static final int MSG_LAST_ID = MSG_FIRST_ID + 999; private static WeakReference waitingForOpen; @@ -113,6 +119,11 @@ public boolean handleMessage(Message msg) result.setResult(null); // signal closed return true; } + case MSG_GET_SAFE_AREA_PADDING: { + AsyncResult result = (AsyncResult) msg.obj; + result.setResult(handleGetSafeAreaPadding()); + return true; + } default: { return super.handleMessage(msg); } @@ -307,6 +318,11 @@ public void onWindowFocusChange(boolean focused) fireEvent((focused) ? TiC.EVENT_FOCUS : TiC.EVENT_BLUR, null, false); } + public void fireSafeAreaChangedEvent() + { + TiUIHelper.firePostLayoutEvent(this); + } + // clang-format off @Kroll.method @Kroll.setProperty @@ -426,6 +442,99 @@ public ActivityProxy getWindowActivityProxy() } } + // clang-format off + @Kroll.method + @Kroll.getProperty + public KrollDict getSafeAreaPadding() + // clang-format on + { + KrollDict dictionary; + if (TiApplication.isUIThread()) { + dictionary = handleGetSafeAreaPadding(); + } else { + dictionary = (KrollDict) TiMessenger.sendBlockingMainMessage( + getMainHandler().obtainMessage(MSG_GET_SAFE_AREA_PADDING), getActivity()); + } + return dictionary; + } + + private KrollDict handleGetSafeAreaPadding() + { + // Initialize safe-area padding to zero. (ie: no padding) + double paddingLeft = 0; + double paddingTop = 0; + double paddingRight = 0; + double paddingBottom = 0; + + // Fetch safe-area from activity. (Returned safe-area is relative to root decor view.) + Rect safeAreaRect = null; + Activity activity = getActivity(); + if (activity instanceof TiBaseActivity) { + safeAreaRect = ((TiBaseActivity) activity).getSafeAreaRect(); + } + + // Fetch content view that the safe-area should be made relative to. + View contentView = null; + if (this.tabGroup != null) { + // This window is displayed within a TabGroup. Use the TabGroup's container view. + // Note: Don't use this window's content view because if its tab is not currently selected, + // then this window's view coordinates will be offscreen and won't intersect safe-area. + TiUIView uiView = this.tabGroup.peekView(); + if (uiView != null) { + contentView = uiView.getNativeView(); + } + } + if ((contentView == null) && (this.view != null)) { + // Use this window's content view. + contentView = this.view.getNativeView(); + } + + // Calculate safe-area padding relative to content view. + if ((contentView != null) && (safeAreaRect != null)) { + // Get the content view's x/y position relative to window's root decor view. + // Note: Do not use the getLocationInWindow() method, because it'll fetch the view's current position + // during transition animations. Such as when the ActionBar is being shown/hidden. + int contentX = contentView.getLeft(); + int contentY = contentView.getTop(); + { + ViewParent viewParent = contentView.getParent(); + for (; viewParent instanceof View; viewParent = viewParent.getParent()) { + View view = (View) viewParent; + contentX += view.getLeft() - view.getScrollX(); + contentY += view.getTop() - view.getScrollY(); + } + } + + // Convert safe-area coordinates to be relative to content view. + safeAreaRect.offset(-contentX, -contentY); + + // Calculate the safe-area padding relative to the content view. + // Do not allow the padding to be less than zero on any side. + paddingLeft = (double) Math.max(safeAreaRect.left, 0); + paddingTop = (double) Math.max(safeAreaRect.top, 0); + paddingRight = (double) Math.max(contentView.getWidth() - safeAreaRect.right, 0); + paddingBottom = (double) Math.max(contentView.getHeight() - safeAreaRect.bottom, 0); + + // Convert padding values from pixels to Titanium's default units. + TiDimension leftDimension = new TiDimension(paddingLeft, TiDimension.TYPE_LEFT); + TiDimension topDimension = new TiDimension(paddingTop, TiDimension.TYPE_TOP); + TiDimension rightDimension = new TiDimension(paddingRight, TiDimension.TYPE_RIGHT); + TiDimension bottomDimension = new TiDimension(paddingBottom, TiDimension.TYPE_BOTTOM); + paddingLeft = leftDimension.getAsDefault(contentView); + paddingTop = topDimension.getAsDefault(contentView); + paddingRight = rightDimension.getAsDefault(contentView); + paddingBottom = bottomDimension.getAsDefault(contentView); + } + + // Return the result via a titanium "ViewPadding" dictionary. + KrollDict dictionary = new KrollDict(); + dictionary.put(TiC.PROPERTY_LEFT, paddingLeft); + dictionary.put(TiC.PROPERTY_TOP, paddingTop); + dictionary.put(TiC.PROPERTY_RIGHT, paddingRight); + dictionary.put(TiC.PROPERTY_BOTTOM, paddingBottom); + return dictionary; + } + protected abstract void handleOpen(KrollDict options); protected abstract void handleClose(KrollDict options); protected abstract Activity getWindowActivity(); diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java new file mode 100644 index 00000000000..037e27a3953 --- /dev/null +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java @@ -0,0 +1,108 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2009-2018 by Axway, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ + +package org.appcelerator.titanium.view; + +import android.content.res.Configuration; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.Toolbar; +import android.view.View; +import android.view.ViewGroup; +import org.appcelerator.titanium.util.TiRHelper; + +/** + * Updates an activity's default Google ActionBar height and font size when the orientation changes. + * Intended to be used by activities which override "configChanges" for orientation since the system won't + * automatically update the ActionBar for you in that case (56dp for portrait and 48dp for landscape). + *

+ * Instances of this class can only be created by the static from() method, which is expected to be called + * within an Activity.onCreate() method. The activity which owns this instance is then expected to call + * this instance's onConfigurationChanged() method when the activity's equivalent method has been invoked. + */ +public class TiActionBarStyleHandler +{ + /** The default Android log tag name to be used by this class. */ + private static final String TAG = "TiActionBarStyleHandler"; + + /** Style handler to use if Google's ActionBar is implemented via a toolbar. Will be null if not. */ + private TiToolbarStyleHandler toolbarStyleHandler; + + /** Constructor made private to force caller to use the static from() method. */ + private TiActionBarStyleHandler() + { + } + + /** + * To be called by the owner when the activity's overriden onConfigurationChanged() method has been called. + * Updates the ActionBar's height and font size base on the given configuration. + * @param newConfig The udpated configuration applied to the activity. + */ + public void onConfigurationChanged(Configuration newConfig) + { + // Do not continue if we don't have access to the ActionBar. + if (this.toolbarStyleHandler == null) { + return; + } + + // Update the ActionBar's toolbar style/size. + this.toolbarStyleHandler.onConfigurationChanged(newConfig); + + // Update the toolbar's layout height if currently set to a pixel value. + // Note: We don't want to change it if set to WRAP_CONTENT, but I've never seen ActionBar do this. + Toolbar toolbar = this.toolbarStyleHandler.getToolbar(); + ViewGroup.LayoutParams layoutParams = toolbar.getLayoutParams(); + if ((layoutParams != null) && (layoutParams.height > 0)) { + int minHeight = toolbar.getMinimumHeight(); + if (minHeight > 0) { + layoutParams.height = minHeight; + toolbar.requestLayout(); + toolbar.requestFitSystemWindows(); + } + } + } + + /** + * Searches the given activity for the default Google ActionBar and returns a new handler instance if found. + * This method is expected to be called from an activity's onCreate() method. + * @param activity The activity to search for an ActionBar from. Can be null. + * @return + * Returns a new handler if an ActionBar was found. + *

+ * Returns null if ActionBar not found or if given a null argument + */ + public static TiActionBarStyleHandler from(AppCompatActivity activity) + { + // Validate argument. + if (activity == null) { + return null; + } + + // Attempt to find Google's default ActionBar from the given activity. + // Note: It won't have one if using a "Window.FEATURE_NO_TITLE" theme. + TiToolbarStyleHandler toolbarStyleHandler = null; + try { + // Check if ActionBar is using a Toolbar via AppCompat "abc_screen_toolbar.xml" theme. + int actionBarId = TiRHelper.getResource("id.action_bar"); + View view = activity.findViewById(actionBarId); + if (view instanceof Toolbar) { + // Toolbar found. Set up a Toolbar style handler. + toolbarStyleHandler = new TiToolbarStyleHandler((Toolbar) view); + } + } catch (Exception ex) { + } + + // Do not continue if ActionBar not found. + if (toolbarStyleHandler == null) { + return null; + } + + // Set up an instance of this class and return it. + TiActionBarStyleHandler actionBarStyleHandler = new TiActionBarStyleHandler(); + actionBarStyleHandler.toolbarStyleHandler = toolbarStyleHandler; + return actionBarStyleHandler; + } +} diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java new file mode 100644 index 00000000000..ee3dc9ea5d4 --- /dev/null +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java @@ -0,0 +1,390 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2009-2018 by Axway, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ + +package org.appcelerator.titanium.view; + +import android.graphics.Rect; +import android.os.Build; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; +import android.view.View; +import android.view.Window; +import android.view.WindowInsets; + +/** Tracks safe-area inset changes for a given activity. */ +public class TiActivitySafeAreaMonitor +{ + /** + * Listener which gets invoked by "TiActivitySafeAreaMonitor" when it's safe-area has changed. + * The updated safe-area can be retrieved by calling the monitor's getSafeAreaRect() method. + *

+ * An instance of this type is expected to be passed to the setOnChangedListener() method. + */ + public interface OnChangedListener + { + void onChanged(TiActivitySafeAreaMonitor monitor); + } + + /** The activity to be monitored. */ + private AppCompatActivity activity; + + /** Set true if monitor's start() method was called. Set false if stopped. */ + private boolean isRunning; + + /** Set true to add ActionBar height to top inset and exclude from safe-area. */ + private boolean isActionBarAddedAsInset; + + /** Safe-area change listener given by the owner of this monitor. */ + private OnChangedListener changeListener; + + /** Listens for the root decor view's layout changes. */ + private View.OnLayoutChangeListener viewLayoutListener; + + /** Listens for the root decor view's inset changes. Will be null on Android 4.4 and older versions. */ + private View.OnApplyWindowInsetsListener viewInsetListener; + + /** Pixel width of the inset overlapping the left side of the window's content. */ + private int insetLeft; + + /** Pixel height of the inset overlapping the top of the window's content. Does not include ActionBar height. */ + private int insetTop; + + /** Pixel width of the inset overlapping the right side of the window's content. */ + private int insetRight; + + /** Pixel height of the inset overlapping the bottom of the window's content. */ + private int insetBottom; + + /** Region between the screen insets in pixels, relative to the root decor view. */ + private Rect safeArea; + + /** + * Creates an object used to track safe-area region changes for the given activity. + * @param activity The activity to be monitored. Cannot be null. + */ + public TiActivitySafeAreaMonitor(AppCompatActivity activity) + { + // Validate. + if (activity == null) { + throw new NullPointerException(); + } + + // Initialize member variables. + this.activity = activity; + this.isActionBarAddedAsInset = true; + + // Set up a listener for root decor view's layout changes. + this.viewLayoutListener = new View.OnLayoutChangeListener() { + @Override + public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, + int oldTop, int oldRight, int oldBottom) + { + // Updates safe-area based on view's newest size and position. + // Note: On Android 4.4 and below, we have to poll for inset on every layout change. + if (TiActivitySafeAreaMonitor.this.viewInsetListener != null) { + updateUsingCachedInsets(); + } else { + update(); + } + } + }; + + // Set up a listener for root decor view's inset changes. + // Note: This is only available on Android 5.0 and higher. + if (Build.VERSION.SDK_INT >= 20) { + this.viewInsetListener = new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets insets) + { + // Validate. + if (view == null) { + return insets; + } + + // Update safe-area using given insets. + updateUsing(insets); + + // Let the view handle the insets. + // Allows the View.setFitsSystemWindows(true) method to work. + return view.onApplyWindowInsets(insets); + } + }; + } + } + + /** + * Gets the activity that's being monitored. + * @return Returns the activity that's being monitored. + */ + public AppCompatActivity getActivity() + { + return this.activity; + } + + /** + * Determines if ActionBar height (if shown) is added to the top inset and excluded from the safe-area. + * @return Returns true if ActionBar is added to the top inset. Returns false if ActionBar is ignored. + */ + public boolean isActionBarAddedAsInset() + { + return this.isActionBarAddedAsInset; + } + + /** + * Sets whether or not the ActionBar height (if shown) should be added as a top inset + * and excluded from the safe-area. This is set true by default. + *

+ * Expected to be set true when using Google's default ActionBar. + *

+ * Intended to be set false when using a custom toolbar via AppCompatActivity.setSupportActionBar() since + * that toolbar will be part of the activity's content view. + * @param value Set true to add the ActionBar to the top inset. Set false to ignore ActionBar. + */ + public void setActionBarAddedAsInset(boolean value) + { + // Do not continue if setting is not changing. + if (value == this.isActionBarAddedAsInset) { + return; + } + + // Store new setting and update safe-area. + this.isActionBarAddedAsInset = value; + update(); + } + + /** + * Gets the listener assigned via the setOnChangedListener() method. + * @return Returns the assigned listener. Returns null if no listener has been assigned. + */ + public TiActivitySafeAreaMonitor.OnChangedListener getOnChangedListener() + { + return this.changeListener; + } + + /** + * Sets a listener to be invoked when the safe-area has changed. + * Given listener will only be invoked when the monitor has been started. + * @param listener The listener to be assigned. Can be set null to remove the last assigned listener. + */ + public void setOnChangedListener(TiActivitySafeAreaMonitor.OnChangedListener listener) + { + this.changeListener = listener; + } + + /** + * Gets the activity's safe-area in pixels, relative to the root decor view. + * @return Returns the safe-area. Returns null if activity's root decor view is not attached. + */ + public Rect getSafeAreaRect() + { + // If this monitor is not currently running, then fetch the safe-area. + if (this.isRunning == false) { + update(); + } + + // If safe-are is null, then we were unable to fetch activity's root decor view. + // This will happen if the activity has been destroyed before we had a chance to access it. + if (this.safeArea == null) { + return null; + } + + // Return a copy of the cached safe-area. + // We do this because the "Rect" class is mutable. + return new Rect(this.safeArea); + } + + /** + * Gets the activity's root decor view, if still available. + * @return Returns the activity's root decor view. Returns null if the activity has been destroyed. + */ + private View getDecorView() + { + Window window = this.activity.getWindow(); + if (window != null) { + return window.getDecorView(); + } + return null; + } + + /** + * Fetches the ActionBar height, but only if shown and "isActionBarAddedAsInset" is set true. + * Intended to be added to the top inset height. + * @return Returns the ActionBar pixel height if applicable. + */ + private int getActionBarInsetHeight() + { + if (this.isActionBarAddedAsInset) { + ActionBar actionBar = this.activity.getSupportActionBar(); + if ((actionBar != null) && actionBar.isShowing()) { + return actionBar.getHeight(); + } + } + return 0; + } + + /** + * Determines if this monitor has been started/stopped. + * @return + * Returns true if the start() method was called and this object is monitor for safe-area changes. + *

+ * Returns false if the stop() method was called or monitor has never been started. + */ + public boolean isRunning() + { + return this.isRunning; + } + + /** + * Starts listening for activity safe-area inset changes. + * This method is expected to be called after the Activity.onCreate() method has been called. + *

+ * The listener passed to setOnChangedListener() won't be invoked until this monitor has been started. + */ + public void start() + { + // Do not continue if already started. + if (this.isRunning) { + return; + } + + // Fetch the activity's root decor view, if still available. + View rootView = getDecorView(); + if (rootView == null) { + return; + } + + // Subscribe to root view's events. + this.isRunning = true; + rootView.addOnLayoutChangeListener(this.viewLayoutListener); + if (this.viewInsetListener != null) { + rootView.setOnApplyWindowInsetsListener(this.viewInsetListener); + } + + // Fetch root view's current safe-area. + update(); + } + + /** + * Stops listening for activity safe-area inset changes. + *

+ * The listener passed to setOnChangedListener() won't be invoked while stopped. + */ + public void stop() + { + // Do not continue if already stopped. + if (this.isRunning == false) { + return; + } + + // Fetch the activity's root decor view, if still available. + View rootView = getDecorView(); + if (rootView == null) { + return; + } + + // Unsubscribe from root view's events. + this.isRunning = false; + rootView.removeOnLayoutChangeListener(this.viewLayoutListener); + if (this.viewInsetListener != null) { + rootView.setOnApplyWindowInsetsListener(null); + } + } + + /** + * Updates this object's inset and safe-area member variables by fetching this info from the root view. + *

+ * Will invoke the assigned OnChangedListener if the safe-area size/position has changed. + */ + private void update() + { + // Fetch the activity's root decor view, if still available. + View rootView = getDecorView(); + if (rootView == null) { + return; + } + + // Fetch the currently applied insets and update safe-area. + // Note: Google's internal code comments states that getWindowVisibleDisplayFrame() is "broken". + // It's proven to work for us, but let's avoid this API in case of any unknown edge cases. + if (Build.VERSION.SDK_INT >= 23) { + updateUsing(rootView.getRootWindowInsets()); + } else { + Rect rect = new Rect(rootView.getLeft(), rootView.getTop(), rootView.getRight(), rootView.getBottom()); + rootView.getWindowVisibleDisplayFrame(rect); + this.insetLeft = Math.max(rect.left - rootView.getLeft(), 0); + this.insetTop = Math.max(rect.top - rootView.getTop(), 0); + this.insetRight = Math.max(rootView.getRight() - rect.right, 0); + this.insetBottom = Math.max(rootView.getBottom() - rect.bottom, 0); + updateUsingCachedInsets(); + } + } + + /** + * Updates this object's inset and safe-area member variables using the given Android window insets. + * Expected to be called when the root view's onApplyWindowInsets() method has been called. + *

+ * Will invoke the assigned OnChangedListener if the safe-area size/position has changed. + */ + private void updateUsing(WindowInsets insets) + { + // Update using the system-window insets. + // Note: Ignore the "stable" insets. They're used for fullscreen or immersive mode, + // indicating where the offscreen status bar and navigation bar will slide-in to. + if (insets != null) { + this.insetLeft = insets.getSystemWindowInsetLeft(); + this.insetTop = insets.getSystemWindowInsetTop(); + this.insetRight = insets.getSystemWindowInsetRight(); + this.insetBottom = insets.getSystemWindowInsetBottom(); + } else { + this.insetLeft = 0; + this.insetTop = 0; + this.insetRight = 0; + this.insetBottom = 0; + } + updateUsingCachedInsets(); + } + + /** + * Updates the stored safe-area using this object's currently assigned inset member variables. + *

+ * Will invoke the assigned OnChangedListener if the safe-area size/position has changed. + */ + private void updateUsingCachedInsets() + { + // Fetch the activity's root decor view. + View rootView = getDecorView(); + if (rootView == null) { + return; + } + + // Calculate the safe-area, which is the region between the insets. + // Note: We must dynamically add ActionBar height here (if enabled) since its + // visbility and height changes can only be detected via an onLayoutChange() event. + Rect rect = new Rect(); + rect.left = this.insetLeft; + rect.top = this.insetTop + getActionBarInsetHeight(); + rect.right = rootView.getWidth() - this.insetRight; + rect.bottom = rootView.getHeight() - this.insetBottom; + + // Make sure safe-area does not have a negative width and height. + rect.bottom = Math.max(rect.top, rect.bottom); + rect.right = Math.max(rect.left, rect.right); + + // Do not continue if the safe-area hasn't changed. + if (rect.equals(this.safeArea)) { + return; + } + + // Update our cached safe-area member variable. + this.safeArea = rect; + + // Notify owner that safe-area has changed. + if (this.changeListener != null) { + this.changeListener.onChanged(this); + } + } +} diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiCompositeLayout.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiCompositeLayout.java index 09ff31d96a5..9519a6e004b 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiCompositeLayout.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiCompositeLayout.java @@ -25,6 +25,7 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.OnHierarchyChangeListener; +import android.view.WindowInsets; /** * Base layout class for all Titanium views. @@ -446,6 +447,35 @@ protected int getViewHeightPadding(View child, int parentHeight) return padding; } + /** + * Called when the window insets (translucent status bar, navigation bar, or screen notches) have changed + * size or position. Can be used to layout views around the insets if setFitsSystemWindows() is true. + *

+ * Layouts such as "FrameLayout", "LinearLayout", and "RelativeLayout" do not pass insets to child views. + * So, Titanium's "TiCompositeLayout" needs to do this manually. + * @param insets The new insets to be applied to this layout and its child views. Can be null. + * @return Returns the given insets minus the insets consumed by this view layout. + */ + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) + { + // Validate. + if (insets == null) { + return null; + } + + // Apply insets to all child views and don't let them consume given insets. + // We must do this since a "composite" layout supports overlapping views. + final int childCount = getChildCount(); + for (int index = 0; index < childCount; index++) { + View childView = getChildAt(index); + if (childView != null) { + childView.dispatchApplyWindowInsets(insets); + } + } + return insets; + } + @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { @@ -1063,6 +1093,70 @@ public LayoutParams() index = Integer.MIN_VALUE; } + + /** + * Determines if layout parameters are set up to Ti.UI.SIZE (aka: WRAP_CONTENT) the view's width. + * @return Returns true if view's width should wrap content. Returns false if not. + */ + public boolean hasAutoSizedWidth() + { + // Not auto-sized if "width" is set to a value or Ti.UI.FILL. + if ((this.optionWidth != null) || this.autoFillsWidth) { + return false; + } + + // We are auto-sized if "width" was explicitly set to Ti.UI.SIZE. + if (this.sizeOrFillWidthEnabled) { + return true; + } + + // The "width" property was not set. Check the left/center/right pins. + // If no more than 1 pin is set, then we're auto-sized and that pin is used to position the view. + // Note: If 2 pins are set, then that is used to set the view's width between the pins. + int pinCount = 0; + if (this.optionLeft != null) { + pinCount++; + } + if (this.optionCenterX != null) { + pinCount++; + } + if (this.optionRight != null) { + pinCount++; + } + return (pinCount < 2); + } + + /** + * Determines if layout parameters are set up to Ti.UI.SIZE (aka: WRAP_CONTENT) the view's height. + * @return Returns true if view's height should wrap content. Returns false if not. + */ + public boolean hasAutoSizedHeight() + { + // Not auto-sized if "height" is set to a value or Ti.UI.FILL. + if ((this.optionHeight != null) || this.autoFillsHeight) { + return false; + } + + // We are auto-sized if "height" was explicitly set to Ti.UI.SIZE. + if (this.sizeOrFillHeightEnabled) { + return true; + } + + // The "height" property was not set. Check the top/center/bottom pins. + // If no more than 1 pin is set, then we're auto-sized and that pin is used to position the view. + // Note: If 2 pins are set, then that is used to set the view's height between the pins. + int pinCount = 0; + if (this.optionTop != null) { + pinCount++; + } + if (this.optionCenterY != null) { + pinCount++; + } + if (this.optionBottom != null) { + pinCount++; + } + return (pinCount < 2); + } } protected boolean isVerticalArrangement() diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java new file mode 100644 index 00000000000..fd64ad381e8 --- /dev/null +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java @@ -0,0 +1,114 @@ +/** + * Appcelerator Titanium Mobile + * Copyright (c) 2009-2018 by Axway, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ + +package org.appcelerator.titanium.view; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.support.v7.widget.Toolbar; +import java.lang.reflect.Field; +import org.appcelerator.kroll.common.Log; +import org.appcelerator.titanium.util.TiRHelper; + +/** + * Updates a toolbar's height and font size when the orientation changes. + * Intended to be used by activities which override "configChanges" for orientation since the system won't + * automatically update toolbars in that case (56dp for portrait and 48dp for landscape). + *

+ * The owner of this handler is expected to call its onConfigurationChanged() method when its activity + * equivalent method has been invoked. + */ +public class TiToolbarStyleHandler +{ + /** The default Android log tag name to be used by this class. */ + private static final String TAG = "TiToolbarStyleHandler"; + + /** The toolbar to be updated with the newest style/size. */ + private Toolbar toolbar; + + /** + * Creates a new handler for the given toolbar. + * @param toolbar The toolbar to be resized by this handler. Cannot be null. + */ + public TiToolbarStyleHandler(Toolbar toolbar) + { + if (toolbar == null) { + throw new NullPointerException(); + } + + this.toolbar = toolbar; + } + + /** + * Gets the toolbar that is being handled by this instance. + * @return Returns the toolbar being handled. + */ + public Toolbar getToolbar() + { + return this.toolbar; + } + + /** + * To be called by the owner when the activity/view's overriden onConfigurationChanged() method has been called. + * Updates the toolbar's height and font size base on the given configuration. + * @param newConfig The udpated configuration applied to the activity/view. + */ + public void onConfigurationChanged(Configuration newConfig) + { + try { + Context context = this.toolbar.getContext(); + TypedArray typedArray = null; + + // Fetch the toolbar's theme resource ID. + // TODO: We shouldn't assume the toolbar theme. We may want to make this settable in the future. + int styleResourceId = TiRHelper.getResource("style.Widget_AppCompat_Toolbar"); + + // Update the title font size and other styles. + int titleAttributeId = TiRHelper.getResource("attr.titleTextAppearance"); + typedArray = context.obtainStyledAttributes(styleResourceId, new int[] { titleAttributeId }); + int titleResourceId = typedArray.getResourceId(0, 0); + if (titleResourceId != 0) { + this.toolbar.setTitleTextAppearance(context, titleResourceId); + } + typedArray.recycle(); + + // Update the subtitle font size and other styles. + int subtitleAttributeId = TiRHelper.getResource("attr.subtitleTextAppearance"); + typedArray = context.obtainStyledAttributes(styleResourceId, new int[] { subtitleAttributeId }); + int subtitleResourceId = typedArray.getResourceId(0, 0); + if (subtitleResourceId != 0) { + this.toolbar.setSubtitleTextAppearance(context, subtitleResourceId); + } + typedArray.recycle(); + + // Update the toolbar height. + int barSizeAttributeId = TiRHelper.getResource("attr.actionBarSize"); + typedArray = context.obtainStyledAttributes(new int[] { barSizeAttributeId }); + int barSize = typedArray.getDimensionPixelSize(0, 0); + if (barSize > 0) { + this.toolbar.setMinimumHeight(barSize); + } + typedArray.recycle(); + + // Update the toolbar's undocumented max button height. + // Note: Ideally, we should not modify a private member variable like this. + // Unfortunately, we have to since Google's default ActionBar can internally use a Toolbar. + // TODO: In the future we should replace Activity's ActionBar with Toolbar and use a custom + // theme which replaces "maxButtonHeight" value with -1 to avoid this issue. + Field field = Toolbar.class.getDeclaredField("mMaxButtonHeight"); + field.setAccessible(true); + field.set(this.toolbar, barSize); + + // Redraw the toolbar with the above changes. + this.toolbar.requestLayout(); + + } catch (Exception ex) { + Log.e(TAG, "Failed to resize Toolbar.", ex); + } + } +} diff --git a/apidoc/Titanium/UI/Android/Android.yml b/apidoc/Titanium/UI/Android/Android.yml index 5f2d536b745..3c5742a44e3 100644 --- a/apidoc/Titanium/UI/Android/Android.yml +++ b/apidoc/Titanium/UI/Android/Android.yml @@ -30,6 +30,32 @@ methods: format. properties: + - name: FLAG_TRANSLUCENT_NAVIGATION + summary: Window flag which makes the Android system's navigation bar semi-transparent. + description: | + When assigned to , this flag will make the Android + system's bottom navigation bar semi-transparent. This flag will only work if you also set the + [Window.extendSafeArea](Titanium.UI.Window.extendSafeArea) property to `true`. + + This flag is only supported by Android 4.4 and newer OS versions. This flag is ignored on older versions. + On Android 4.4, navigation bar can only be translucent in portrait mode while newer OS versions support + this flag for all orientations. + type: Number + permission: read-only + since: "7.5.0" + osver: {android: {min: "4.4"}} + - name: FLAG_TRANSLUCENT_STATUS + summary: Window flag which makes the Android system's top status bar semi-transparent. + description: | + When assigned to , this flag will make the Android + system's top status bar semi-transparent. This flag will only work if you also set the + property to `true`. + + This flag is only supported by Android 4.4 and newer OS versions. This flag is ignored on older versions. + type: Number + permission: read-only + since: "7.5.0" + osver: {android: {min: "4.4"}} - name: GRAVITY_AXIS_CLIP summary: Raw bit controlling whether the right/bottom edge is clipped to its container, based on the gravity direction being applied. type: Number diff --git a/apidoc/Titanium/UI/Window.yml b/apidoc/Titanium/UI/Window.yml index 8bb5c2fddf0..5be3302252a 100644 --- a/apidoc/Titanium/UI/Window.yml +++ b/apidoc/Titanium/UI/Window.yml @@ -441,24 +441,25 @@ properties: - name: extendSafeArea summary: | - Specifies whether the content (subviews) of the window will render inside the safe-area or not. - Only used in iOS 11.0 and later. + Specifies whether the screen insets/notches are allowed to overlap the window's content or not. description: | - If set `true` (the default), then the content of the window will be extended to fill the whole screen and - under the system's UI elements (such as the top status-bar) and physical obstructions (such as the iPhone X - rounded corners and top sensor housing). In this case, it is the app developer's responsibility to position - views so that they're unobstructed. + If set `true`, then the contents of the window will be extended to fill the whole screen and allow the + system's UI elements (such as a translucent status-bar) and physical obstructions (such as the iPhone X + rounded corners and top sensor housing) to overlap the window's content. In this case, it is the app + developer's responsibility to position views so that they're unobstructed. On Android, you can use the + [Window.safeAreaPadding](Titanium.UI.Window.safeAreaPadding) property after the window has been opened to + layout your content within the insets. If set `false`, then the window's content will be laid out within the safe-area and its child views will be unobstructed. For example, you will not need to position a view below the top status-bar. Read more about the safe-area layout-guide in the [Human Interface Guidelines](https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/). - platforms: [iphone, ipad] + platforms: [android, iphone, ipad] type: Boolean - default: true - since: 6.3.0 + default: `true` on iOS. `false` on Android. + since: {android: "7.5.0", iphone: "6.3.0", ipad: "6.3.0"} availability: creation - osver: {ios: {min: "11.0"}} + osver: {android: {min: "4.4"}, ios: {min: "11.0"}} - name: fullscreen summary: Boolean value indicating if the window is fullscreen. @@ -847,6 +848,51 @@ properties: since: "3.3.0" type: Array + - name: safeAreaPadding + platforms: [android] + summary: The padding needed to safely display content without it being overlapped by the screen insets and notches. + description: | + When setting [Window.extendSafeArea](Titanium.UI.Window.extendSafeArea) to `true`, the system's insets + such as a translucent status bar, translucent navigation bar, and/or camera notches will be allowed to + overlay on top of the window's content. In this case, it is the app developer's responsibility to + prevent these insets from overlapping key content such as buttons. This property provides the amount of + space needed to be added to the left, top, right, and bottom edges of the window root view to do this. + + This property won't return values greater than zero until the window has been opened. It is recommended + that you read this property via a event listener since the padding values can + change when when the app's orientation changes or when showing/hiding the action bar. + + If the [Window.extendSafeArea](Titanium.UI.Window.extendSafeArea) property is set `false`, then the + returned padding will be all zeros since the root content will be positioned between all insets. + + Below is an example on how to set up a safe-area view container using this property. + + // Set up a window with a translucent top status bar and translucent nav bar. + // This will only work on Android 4.4 and newer OS versions. + var win = Ti.UI.createWindow({ + extendSafeArea: true, + theme: 'Theme.AppCompat.NoTitleBar', + windowFlags: Ti.UI.Android.FLAG_TRANSLUCENT_NAVIGATION | Ti.UI.Android.FLAG_TRANSLUCENT_STATUS + }); + + // Set up a safe-area view to be layed out between the system insets. + // You should use this as a container for child views. + var safeAreaView = Ti.UI.createView({ + backgroundColor: 'green' + }); + win.add(safeAreaView); + win.addEventListener('postlayout', function() { + // Update the safe-area view's dimensions after every 'postlayout' event. + safeAreaView.applyProperties(win.safeAreaPadding); + }); + + // Open the window. + win.open(); + + type: Dimension + permission: read-only + since: "7.5.0" + - name: shadowImage summary: Shadow image for the navigation bar, specified as a URL to a local image.. description: | @@ -1075,9 +1121,14 @@ properties: - name: windowFlags summary: Additional flags to set on the Activity Window. description: | - Set the flags of the window, as per the WindowManager.LayoutParams flags. - See [WindowManager.LayoutParams](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html) for a - list of supported flags. Setting to true automatically sets the [WindowManager.LayoutParams.FLAG_FULLSCREEN](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_FULLSCREEN) + Sets flags such as and + . When using multiple flags, you must bitwise-or them together. + + See [WindowManager.LayoutParams](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html) for + list of additional flags that you can assign to this property. You can assign these Java flags to this property + by using their numeric constant. + + Setting to `true` automatically sets the [WindowManager.LayoutParams.FLAG_FULLSCREEN](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_FULLSCREEN) flag. Setting to true automatically sets the [WindowManager.LayoutParams.FLAG_SECURE](https://developer.android.com/reference/android/view/WindowManager.LayoutParams.html#FLAG_SECURE) flag. platforms: [android] type: Number diff --git a/tests/Resources/ti.ui.window.addontest.js b/tests/Resources/ti.ui.window.addontest.js new file mode 100644 index 00000000000..625f167e334 --- /dev/null +++ b/tests/Resources/ti.ui.window.addontest.js @@ -0,0 +1,68 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2011-Present by Appcelerator, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* global Ti */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +var should = require('./utilities/assertions'); + +describe('Titanium.UI.Window', function () { + var win; + + this.timeout(5000); + + afterEach(function () { + if (win) { + win.close(); + } + win = null; + }); + + it.android('.safeAreaPadding with extendSafeArea false', function (finish) { + win = Ti.UI.createWindow({ + extendSafeArea: false, + }); + win.addEventListener('postlayout', function () { + try { + var padding = win.safeAreaPadding; + should(padding).be.a.Object; + should(padding.left).be.eql(0); + should(padding.top).be.eql(0); + should(padding.right).be.eql(0); + should(padding.bottom).be.eql(0); + finish(); + } catch (err) { + finish(err); + } + }); + win.open(); + }); + + // This test will only pass on Android 4.4 and higher since older versions do not support translucent bars. + it.android('.safeAreaPadding with extendSafeArea true', function (finish) { + win = Ti.UI.createWindow({ + extendSafeArea: true, + theme: 'Theme.AppCompat.NoTitleBar', + orientationModes: [ Ti.UI.PORTRAIT ], + windowFlags: Ti.UI.Android.FLAG_TRANSLUCENT_STATUS | Ti.UI.Android.FLAG_TRANSLUCENT_NAVIGATION + }); + win.addEventListener('postlayout', function () { + try { + var padding = win.safeAreaPadding; + should(padding).be.a.Object; + should(padding.top).be.greaterThan(0); + should(padding.bottom).be.greaterThan(0); + should(padding.left >= 0).be.true; + should(padding.right >= 0).be.true; + finish(); + } catch (err) { + finish(err); + } + }); + win.open(); + }); +}); From bb0950ff0911ee9d7aa8e16dfb4cb31c9107f30c Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Wed, 17 Oct 2018 20:02:27 -0700 Subject: [PATCH 2/5] Android: Updated code formatting for [TIMOB-26246] --- .../src/java/org/appcelerator/titanium/TiBaseActivity.java | 2 +- .../titanium/view/TiActivitySafeAreaMonitor.java | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java index 109e057336a..9ec8df26879 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiBaseActivity.java @@ -584,7 +584,7 @@ protected void windowCreated(Bundle savedInstanceState) mask |= WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION; if ((getWindow().getAttributes().flags & mask) != 0) { String message = "You cannot use a translucent status bar or navigation bar unless you " - + "set the window's '" + TiC.PROPERTY_EXTEND_SAFE_AREA + "' property to true."; + + "set the window's '" + TiC.PROPERTY_EXTEND_SAFE_AREA + "' property to true."; Log.w(TAG, message); getWindow().clearFlags(mask); } diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java index ee3dc9ea5d4..9784f2f6a4c 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java @@ -24,8 +24,7 @@ public class TiActivitySafeAreaMonitor *

* An instance of this type is expected to be passed to the setOnChangedListener() method. */ - public interface OnChangedListener - { + public interface OnChangedListener { void onChanged(TiActivitySafeAreaMonitor monitor); } @@ -80,8 +79,8 @@ public TiActivitySafeAreaMonitor(AppCompatActivity activity) // Set up a listener for root decor view's layout changes. this.viewLayoutListener = new View.OnLayoutChangeListener() { @Override - public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, - int oldTop, int oldRight, int oldBottom) + public void onLayoutChange(View view, int left, int top, int right, int bottom, int oldLeft, int oldTop, + int oldRight, int oldBottom) { // Updates safe-area based on view's newest size and position. // Note: On Android 4.4 and below, we have to poll for inset on every layout change. From d06f9f0d7ebffae7092a464154310e4f2130e893 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Thu, 18 Oct 2018 16:09:21 -0700 Subject: [PATCH 3/5] Android: Updated [TIMOB-26246] based on feedback --- .../ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java | 8 ++++---- .../org/appcelerator/titanium/proxy/TiWindowProxy.java | 1 - .../titanium/view/TiActionBarStyleHandler.java | 6 ++++-- .../titanium/view/TiActivitySafeAreaMonitor.java | 2 +- .../appcelerator/titanium/view/TiToolbarStyleHandler.java | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java index 361fe972f1e..c4258a83fbd 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/TabGroupProxy.java @@ -616,17 +616,17 @@ public void fireSafeAreaChangedEvent() // Create a shallow copy of the tab proxy collection owned by this TabGroup. // We need to do this since a tab's event handler can remove a tab, which would break iteration. - ArrayList clonedTabs = null; + ArrayList clonedTabList = null; synchronized (this.tabs) { - clonedTabs = (ArrayList) this.tabs.clone(); + clonedTabList = (ArrayList) this.tabs.clone(); } - if (clonedTabs == null) { + if (clonedTabList == null) { return; } // Fire a safe-area change event for each tab window. - for (TabProxy tab : clonedTabs) { + for (TabProxy tab : clonedTabList) { if (tab != null) { TiWindowProxy window = tab.getWindow(); if (window != null) { diff --git a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java index a355cacf4c8..45e11d9a3f6 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java +++ b/android/titanium/src/java/org/appcelerator/titanium/proxy/TiWindowProxy.java @@ -42,7 +42,6 @@ import android.view.Display; import android.view.View; import android.view.ViewParent; -import android.view.Window; // clang-format off @Kroll.proxy(propertyAccessors = { diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java index 037e27a3953..c3215acec46 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiActionBarStyleHandler.java @@ -12,6 +12,7 @@ import android.support.v7.widget.Toolbar; import android.view.View; import android.view.ViewGroup; +import org.appcelerator.kroll.common.Log; import org.appcelerator.titanium.util.TiRHelper; /** @@ -37,9 +38,9 @@ private TiActionBarStyleHandler() } /** - * To be called by the owner when the activity's overriden onConfigurationChanged() method has been called. + * To be called by the owner when the activity's overridden onConfigurationChanged() method has been called. * Updates the ActionBar's height and font size base on the given configuration. - * @param newConfig The udpated configuration applied to the activity. + * @param newConfig The updated configuration applied to the activity. */ public void onConfigurationChanged(Configuration newConfig) { @@ -93,6 +94,7 @@ public static TiActionBarStyleHandler from(AppCompatActivity activity) toolbarStyleHandler = new TiToolbarStyleHandler((Toolbar) view); } } catch (Exception ex) { + Log.d(TAG, ex.getMessage(), ex); } // Do not continue if ActionBar not found. diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java index 9784f2f6a4c..270cac871dc 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiActivitySafeAreaMonitor.java @@ -362,7 +362,7 @@ private void updateUsingCachedInsets() // Calculate the safe-area, which is the region between the insets. // Note: We must dynamically add ActionBar height here (if enabled) since its - // visbility and height changes can only be detected via an onLayoutChange() event. + // visibility and height changes can only be detected via an onLayoutChange() event. Rect rect = new Rect(); rect.left = this.insetLeft; rect.top = this.insetTop + getActionBarInsetHeight(); diff --git a/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java b/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java index fd64ad381e8..30cab6c1684 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java +++ b/android/titanium/src/java/org/appcelerator/titanium/view/TiToolbarStyleHandler.java @@ -54,9 +54,9 @@ public Toolbar getToolbar() } /** - * To be called by the owner when the activity/view's overriden onConfigurationChanged() method has been called. + * To be called by the owner when the activity/view's overridden onConfigurationChanged() method has been called. * Updates the toolbar's height and font size base on the given configuration. - * @param newConfig The udpated configuration applied to the activity/view. + * @param newConfig The updated configuration applied to the activity/view. */ public void onConfigurationChanged(Configuration newConfig) { From 717528aee54c3593ac025c6893d2df1ddf4fc4b9 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Mon, 22 Oct 2018 19:22:47 -0700 Subject: [PATCH 4/5] Android: Modified [TIMOB-26246] to support multiple toolbars which extend their background - Fixed issue where if multiple toolbars had "extendBackground" true, it would only apply to 1st one. * Had to override default behavior to prevent insets from being consumed. - Fixed regression where toolbar "extendBackground" true would no longer work unless window "extendSafeArea" was also true. * Caused by root Titanium view calling setFitsSystemWindows(true) on startup, instead of default false setting. --- .../modules/titanium/ui/widget/TiToolbar.java | 73 ++++++++++++++++--- 1 file changed, 63 insertions(+), 10 deletions(-) diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java index 0185df07332..8c1ca290fb1 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiToolbar.java @@ -1,5 +1,6 @@ package ti.modules.titanium.ui.widget; +import android.app.Activity; import android.content.res.Configuration; import android.graphics.Color; import android.graphics.Rect; @@ -10,12 +11,15 @@ import android.support.v7.widget.Toolbar; import android.view.View; import android.view.Window; +import android.view.WindowInsets; +import android.view.WindowManager; import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.KrollProxy; import org.appcelerator.kroll.common.AsyncResult; import org.appcelerator.kroll.common.TiMessenger; import org.appcelerator.titanium.TiApplication; +import org.appcelerator.titanium.TiBaseActivity; import org.appcelerator.titanium.TiC; import org.appcelerator.titanium.TiDimension; import org.appcelerator.titanium.proxy.TiViewProxy; @@ -85,6 +89,17 @@ protected void onConfigurationChanged(Configuration newConfig) super.onConfigurationChanged(newConfig); } + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) + { + // Give toolbar a copy of insets and ignore returned "consumed" insets which is set to all zeros. + // Returning zero insets prevents other child views in the hierarchy from receiving system insets, + // which prevents their setFitsSystemWindows(true) from working. (Such as a 2nd toolbar.) + WindowInsets clonedInsets = (insets != null) ? new WindowInsets(insets) : null; + super.onApplyWindowInsets(clonedInsets); + return insets; + } + @Override protected boolean fitSystemWindows(Rect insets) { @@ -113,8 +128,12 @@ protected boolean fitSystemWindows(Rect insets) } } - // Apply the insets to the toolbar. (Google blindly pads view based on these insets.) - return super.fitSystemWindows(insets); + // Apply insets to toolbar. (Google blindly pads the view based on these insets.) + super.fitSystemWindows(insets); + + // Returning false prevents given insets from being consumed. + // Allows other views with setFitsSystemWindows(true) to receive insets. (Such as a 2nd toolbar.) + return false; } }; setNativeView(toolbar); @@ -179,17 +198,51 @@ public void setToolbarExtendBackground() */ private void handleBackgroundExtended() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - Window window = TiApplication.getAppCurrentActivity().getWindow(); - // Compensate for status bar's height - toolbar.setFitsSystemWindows(true); + // This feature is only supported on Android 4.4 or higher. + if (Build.VERSION.SDK_INT < 19) { + return; + } - // Set flags for the current window that allow drawing behind status bar - int flags = window.getDecorView().getSystemUiVisibility(); - flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; - window.getDecorView().setSystemUiVisibility(flags); + // Fetch the currently displayed activity window and its root decor view. + // Note: Will be null if all activities have just been destroyed. + Activity activity = TiApplication.getAppCurrentActivity(); + if (activity == null) { + return; + } + Window window = activity.getWindow(); + if (window == null) { + return; + } + View decorView = window.getDecorView(); + if (decorView == null) { + return; + } + + // Set up root content views to allow top status bar to overlap them. + decorView.setFitsSystemWindows(false); + if (activity instanceof TiBaseActivity) { + View view = ((TiBaseActivity) activity).getLayout(); + if (view != null) { + view.setFitsSystemWindows(false); + } + } + + // Set up toolbar so that it's title and buttons won't be overlapped by the status bar. + // Note that the toolbar will automatically pad its background beneath the status bar as well. + toolbar.setFitsSystemWindows(true); + + // Set flags so that the current window will allow drawing behind the status bar. + int flags = decorView.getSystemUiVisibility(); + flags |= View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + decorView.setSystemUiVisibility(flags); + if (Build.VERSION.SDK_INT >= 21) { window.setStatusBarColor(Color.TRANSPARENT); + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); } + + // Request window to re-fit its views. + toolbar.requestFitSystemWindows(); } /** From d9d7dde2fb9b60bf4827ef96558f81260d00f731 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Tue, 23 Oct 2018 11:01:46 -0700 Subject: [PATCH 5/5] Fixed API doc parse error caused by [TIMOB-26246] --- apidoc/Titanium/UI/Window.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apidoc/Titanium/UI/Window.yml b/apidoc/Titanium/UI/Window.yml index 1c426015bce..9dd6ea6f460 100644 --- a/apidoc/Titanium/UI/Window.yml +++ b/apidoc/Titanium/UI/Window.yml @@ -456,7 +456,7 @@ properties: Read more about the safe-area layout-guide in the [Human Interface Guidelines](https://developer.apple.com/ios/human-interface-guidelines/overview/iphone-x/). platforms: [android, iphone, ipad] type: Boolean - default: `true` on iOS. `false` on Android. + default: {android: false, ios: true} since: {android: "7.5.0", iphone: "6.3.0", ipad: "6.3.0"} availability: creation osver: {android: {min: "4.4"}, ios: {min: "11.0"}}