feat(iOS): Safe Area Support (#6230)

This commit is contained in:
Vasil Chimev
2018-09-28 18:21:50 +03:00
committed by GitHub
parent 46705ee332
commit 982acdc168
39 changed files with 3189 additions and 863 deletions

View File

@@ -586,6 +586,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
public originY: number;
public isEnabled: boolean;
public isUserInteractionEnabled: boolean;
public iosOverflowSafeArea: boolean;
get isLayoutValid(): boolean {
return this._isLayoutValid;
@@ -842,7 +843,7 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
}
_getCurrentLayoutBounds(): { left: number; top: number; right: number; bottom: number } {
return { left: this._oldLeft, top: this._oldTop, right: this._oldRight, bottom: this._oldBottom };
return { left: 0, top: 0, right: 0, bottom: 0 };
}
/**
@@ -879,6 +880,10 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
return undefined;
}
public getSafeAreaInsets(): { left, top, right, bottom } {
return { left: 0, top: 0, right: 0, bottom: 0 };
}
public getLocationInWindow(): Point {
return undefined;
}
@@ -1022,3 +1027,6 @@ isEnabledProperty.register(ViewCommon);
export const isUserInteractionEnabledProperty = new Property<ViewCommon, boolean>({ name: "isUserInteractionEnabled", defaultValue: true, valueConverter: booleanConverter });
isUserInteractionEnabledProperty.register(ViewCommon);
export const iosOverflowSafeAreaProperty = new Property<ViewCommon, boolean>({ name: "iosOverflowSafeArea", defaultValue: false, valueConverter: booleanConverter });
iosOverflowSafeAreaProperty.register(ViewCommon);

View File

@@ -816,7 +816,11 @@ export class View extends ViewCommon {
}
}
export class CustomLayoutView extends View implements CustomLayoutViewDefinition {
export class ContainerView extends View {
public iosOverflowSafeArea: boolean;
}
export class CustomLayoutView extends ContainerView implements CustomLayoutViewDefinition {
nativeViewProtected: android.view.ViewGroup;
public createNativeView() {

View File

@@ -344,6 +344,11 @@ export abstract class View extends ViewBase {
*/
isUserInteractionEnabled: boolean;
/**
* Instruct container view to expand beyond the safe area. This property is iOS specific. Default value: false
*/
iosOverflowSafeArea: boolean;
/**
* Gets is layout is valid. This is a read-only property.
*/
@@ -415,7 +420,6 @@ export abstract class View extends ViewBase {
/**
* Called from onLayout when native view position is about to be changed.
* @param parent This parameter is not used. You can pass null.
* @param left Left position, relative to parent
* @param top Top position, relative to parent
* @param right Right position, relative to parent
@@ -427,7 +431,7 @@ export abstract class View extends ViewBase {
* Measure a child by taking into account its margins and a given measureSpecs.
* @param parent This parameter is not used. You can pass null.
* @param child The view to be measured.
* @param measuredWidth The measured width that the parent layout specifies for this view.
* @param measuredWidth The measured width that the parent layout specifies for this view.
* @param measuredHeight The measured height that the parent layout specifies for this view.
*/
public static measureChild(parent: View, child: View, widthMeasureSpec: number, heightMeasureSpec: number): { measuredWidth: number; measuredHeight: number };
@@ -527,6 +531,11 @@ export abstract class View extends ViewBase {
*/
public createAnimation(options: AnimationDefinition): Animation;
/**
* Returns the iOS safe area insets of this view.
*/
public getSafeAreaInsets(): { left, top, right, bottom };
/**
* Returns the location of this view in the window coordinate system.
*/
@@ -609,7 +618,7 @@ export abstract class View extends ViewBase {
* Called by layout method to cache view bounds.
* @private
*/
_setCurrentLayoutBounds(left: number, top: number, right: number, bottom: number): void;
_setCurrentLayoutBounds(left: number, top: number, right: number, bottom: number): { boundsChanged: boolean, sizeChanged: boolean };
/**
* Return view bounds.
* @private
@@ -700,10 +709,20 @@ export abstract class View extends ViewBase {
_setValue(property: any, value: any): never;
}
/**
* Base class for all UI components that are containers.
*/
export class ContainerView extends View {
/**
* Instruct container view to expand beyond the safe area. This property is iOS specific. Default value: true
*/
public iosOverflowSafeArea: boolean;
}
/**
* Base class for all UI components that implement custom layouts.
*/
export class CustomLayoutView extends View {
export class CustomLayoutView extends ContainerView {
//@private
/**
* @private
@@ -777,6 +796,7 @@ export const originXProperty: Property<View, number>;
export const originYProperty: Property<View, number>;
export const isEnabledProperty: Property<View, boolean>;
export const isUserInteractionEnabledProperty: Property<View, boolean>;
export const iosOverflowSafeAreaProperty: Property<View, boolean>;
export namespace ios {
/**
@@ -784,10 +804,13 @@ export namespace ios {
* @param view The view form which to start the search.
*/
export function getParentWithViewController(view: View): View
export function isContentScrollable(controller: any /* UIViewController */, owner: View): boolean
export function updateAutoAdjustScrollInsets(controller: any /* UIViewController */, owner: View): void
export function updateConstraints(controller: any /* UIViewController */, owner: View): void;
export function layoutView(controller: any /* UIViewController */, owner: View): void;
export function getPositionFromFrame(frame: any /* CGRect */): { left, top, right, bottom };
export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): any /* CGRect */;
export function shrinkToSafeArea(view: View, frame: any /* CGRect */): any /* CGRect */;
export function expandBeyondSafeArea(view: View, frame: any /* CGRect */): any /* CGRect */;
export class UILayoutViewController {
public static initWithOwner(owner: WeakRef<View>): UILayoutViewController;
}

View File

@@ -1,10 +1,11 @@
// Definitions.
import { Point, View as ViewDefinition, dip } from ".";
import { ViewBase } from "../view-base";
import { booleanConverter, Property } from "../view";
import {
ViewCommon, layout, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty,
traceEnabled, traceWrite, traceCategories, traceError, traceMessageType
traceEnabled, traceWrite, traceCategories, traceError, traceMessageType, getAncestor
} from "./view-common";
import { ios as iosBackground, Background } from "../../styling/background";
@@ -24,6 +25,8 @@ const PFLAG_FORCE_LAYOUT = 1;
const PFLAG_MEASURED_DIMENSION_SET = 1 << 1;
const PFLAG_LAYOUT_REQUIRED = 1 << 2;
const majorVersion = iosUtils.MajorVersion;
export class View extends ViewCommon {
nativeViewProtected: UIView;
viewController: UIViewController;
@@ -90,7 +93,15 @@ export class View extends ViewCommon {
}
if (boundsChanged || (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED) {
this.onLayout(left, top, right, bottom);
let position = { left, top, right, bottom };
if (this.nativeViewProtected && majorVersion > 10) {
// on iOS 11+ it is possible to have a changed layout frame due to safe area insets
// get the frame and adjust the position, so that onLayout works correctly
const frame = this.nativeViewProtected.frame;
position = ios.getPositionFromFrame(frame);
}
this.onLayout(position.left, position.top, position.right, position.bottom);
this._privateFlags &= ~PFLAG_LAYOUT_REQUIRED;
}
@@ -139,13 +150,13 @@ export class View extends ViewCommon {
}
public onLayout(left: number, top: number, right: number, bottom: number): void {
//
//
}
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
public _setNativeViewFrame(nativeView: UIView, frame: CGRect): void {
if (!CGRectEqualToRect(nativeView.frame, frame)) {
if (traceEnabled()) {
traceWrite(this + ", Native setFrame: = " + NSStringFromCGRect(frame), traceCategories.Layout);
traceWrite(this + " :_setNativeViewFrame: " + JSON.stringify(ios.getPositionFromFrame(frame)), traceCategories.Layout);
}
this._cachedFrame = frame;
if (this._hasTransfrom) {
@@ -154,13 +165,19 @@ export class View extends ViewCommon {
nativeView.transform = CGAffineTransformIdentity;
nativeView.frame = frame;
nativeView.transform = transform;
}
else {
} else {
nativeView.frame = frame;
}
const adjustedFrame = this.applySafeAreaInsets(frame);
if (adjustedFrame) {
nativeView.frame = adjustedFrame;
}
const boundsOrigin = nativeView.bounds.origin;
nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, frame.size.width, frame.size.height);
const boundsFrame = adjustedFrame || frame;
nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, boundsFrame.size.width, boundsFrame.size.height);
this._raiseLayoutChangedEvent();
this._isLaidOut = true;
} else if (!this._isLaidOut) {
@@ -183,7 +200,7 @@ export class View extends ViewCommon {
}
const nativeView = this.nativeViewProtected;
const frame = CGRectMake(layout.toDeviceIndependentPixels(left), layout.toDeviceIndependentPixels(top), layout.toDeviceIndependentPixels(right - left), layout.toDeviceIndependentPixels(bottom - top));
const frame = ios.getFrameFromPosition({ left, top, right, bottom });
this._setNativeViewFrame(nativeView, frame);
}
@@ -211,6 +228,34 @@ export class View extends ViewCommon {
return false;
}
protected applySafeAreaInsets(frame: CGRect): CGRect {
if (majorVersion <= 10) {
return null;
}
if (!this.iosOverflowSafeArea) {
return ios.shrinkToSafeArea(this, frame);
} else if (this.nativeViewProtected && this.nativeViewProtected.window) {
return ios.expandBeyondSafeArea(this, frame);
}
return null;
}
public getSafeAreaInsets(): { left, top, right, bottom } {
const safeAreaInsets = this.nativeViewProtected && this.nativeViewProtected.safeAreaInsets;
let insets = { left: 0, top: 0, right: 0, bottom: 0 };
if (safeAreaInsets) {
insets.left = layout.round(layout.toDevicePixels(safeAreaInsets.left));
insets.top = layout.round(layout.toDevicePixels(safeAreaInsets.top));
insets.right = layout.round(layout.toDevicePixels(safeAreaInsets.right));
insets.bottom = layout.round(layout.toDevicePixels(safeAreaInsets.bottom));
}
return insets;
}
public getLocationInWindow(): Point {
if (!this.nativeViewProtected || !this.nativeViewProtected.window) {
return undefined;
@@ -374,7 +419,7 @@ export class View extends ViewCommon {
protected _hideNativeModalView(parent: View) {
if (!parent || !parent.viewController) {
traceError("Trying to hide modal view but no parent with viewController specified.")
return;
return;
}
const parentController = parent.viewController;
@@ -508,6 +553,23 @@ export class View extends ViewCommon {
}
}
_getCurrentLayoutBounds(): { left: number; top: number; right: number; bottom: number } {
const nativeView = this.nativeViewProtected;
if (nativeView && !this.isCollapsed) {
const frame = nativeView.frame;
const origin = frame.origin;
const size = frame.size;
return {
left: Math.round(layout.toDevicePixels(origin.x)),
top: Math.round(layout.toDevicePixels(origin.y)),
right: Math.round(layout.toDevicePixels(origin.x + size.width)),
bottom: Math.round(layout.toDevicePixels(origin.y + size.height))
};
} else {
return { left: 0, top: 0, right: 0, bottom: 0 };
}
}
_redrawNativeBackground(value: UIColor | Background): void {
let updateSuspended = this._isPresentationLayerUpdateSuspeneded();
if (!updateSuspended) {
@@ -540,7 +602,17 @@ export class View extends ViewCommon {
}
View.prototype._nativeBackgroundState = "unset";
export class CustomLayoutView extends View {
export class ContainerView extends View {
public iosOverflowSafeArea: boolean;
constructor() {
super();
this.iosOverflowSafeArea = true;
}
}
export class CustomLayoutView extends ContainerView {
nativeViewProtected: UIView;
@@ -582,23 +654,6 @@ export class CustomLayoutView extends View {
child.nativeViewProtected.removeFromSuperview();
}
}
_getCurrentLayoutBounds(): { left: number; top: number; right: number; bottom: number } {
const nativeView = this.nativeViewProtected;
if (nativeView && !this.isCollapsed) {
const frame = nativeView.frame;
const origin = frame.origin;
const size = frame.size;
return {
left: layout.toDevicePixels(origin.x),
top: layout.toDevicePixels(origin.y),
right: layout.toDevicePixels(origin.x + size.width),
bottom: layout.toDevicePixels(origin.y + size.height)
};
} else {
return { left: 0, top: 0, right: 0, bottom: 0 };
}
}
}
export namespace ios {
@@ -611,28 +666,19 @@ export namespace ios {
return view;
}
export function isContentScrollable(controller: UIViewController, owner: View): boolean {
let scrollableContent = (<any>owner).scrollableContent;
if (scrollableContent === undefined) {
const view: UIView = controller.view.subviews.count > 0 ? controller.view.subviews[0] : null;
if (view instanceof UIScrollView) {
scrollableContent = true;
}
}
return scrollableContent === true || scrollableContent === "true";
}
export function updateAutoAdjustScrollInsets(controller: UIViewController, owner: View): void {
const scrollable = isContentScrollable(controller, owner);
owner._automaticallyAdjustsScrollViewInsets = scrollable;
controller.automaticallyAdjustsScrollViewInsets = scrollable;
if (majorVersion <= 10) {
owner._automaticallyAdjustsScrollViewInsets = false;
// This API is deprecated, but has no alternative for <= iOS 10
// Defaults to true and results to appliyng the insets twice together with our logic
// for iOS 11+ we use the contentInsetAdjustmentBehavior property in scrollview
// https://developer.apple.com/documentation/uikit/uiviewcontroller/1621372-automaticallyadjustsscrollviewin
controller.automaticallyAdjustsScrollViewInsets = false;
}
}
export function updateConstraints(controller: UIViewController, owner: View): void {
const root = controller.view;
if (!root.safeAreaLayoutGuide) {
if (majorVersion <= 10) {
const layoutGuide = initLayoutGuide(controller);
(<any>controller.view).safeAreaLayoutGuide = layoutGuide;
}
@@ -651,26 +697,19 @@ export namespace ios {
return layoutGuide;
}
function getStatusBarHeight(viewController?: UIViewController): number {
const app = iosUtils.getter(UIApplication, UIApplication.sharedApplication);
if (!app || app.statusBarHidden) {
return 0;
}
if (viewController && viewController.prefersStatusBarHidden) {
return 0;
}
const statusFrame = app.statusBarFrame;
return Math.min(statusFrame.size.width, statusFrame.size.height);
}
export function layoutView(controller: UIViewController, owner: View): void {
let left: number, top: number, width: number, height: number;
// apply parent page additional top insets if any. The scenario is when there is a parent page with action bar.
const parentPage = getAncestor(owner, "Page");
if (parentPage) {
const parentPageInsetsTop = parentPage.viewController.view.safeAreaInsets.top;
const currentInsetsTop = controller.view.safeAreaInsets.top;
const additionalInsetsTop = parentPageInsetsTop - currentInsetsTop;
const frame = controller.view.frame;
const fullscreenOrigin = frame.origin;
const fullscreenSize = frame.size;
if (additionalInsetsTop > 0) {
const additionalInsets = new UIEdgeInsets({ top: additionalInsetsTop, left: 0, bottom: 0, right: 0 });
controller.additionalSafeAreaInsets = additionalInsets;
}
}
let layoutGuide = controller.view.safeAreaLayoutGuide;
if (!layoutGuide) {
@@ -680,56 +719,104 @@ export namespace ios {
layoutGuide = initLayoutGuide(controller);
}
const safeArea = layoutGuide.layoutFrame;
const safeOrigin = safeArea.origin;
let position = ios.getPositionFromFrame(safeArea);
const safeAreaSize = safeArea.size;
const navController = controller.navigationController;
const navBarHidden = navController ? navController.navigationBarHidden : true;
const scrollable = isContentScrollable(controller, owner);
const hasChildControllers = controller.childViewControllers.count > 0;
if (!(controller.edgesForExtendedLayout & UIRectEdge.Top)) {
const statusBarHeight = getStatusBarHeight(controller);
const navBarHeight = controller.navigationController ? controller.navigationController.navigationBar.frame.size.height : 0;
fullscreenOrigin.y = safeOrigin.y;
fullscreenSize.height -= (statusBarHeight + navBarHeight);
const hasChildViewControllers = controller.childViewControllers.count > 0;
if (hasChildViewControllers) {
const fullscreen = controller.view.frame;
position = ios.getPositionFromFrame(fullscreen);
}
left = safeOrigin.x;
width = safeAreaSize.width;
const safeAreaWidth = layout.round(layout.toDevicePixels(safeAreaSize.width));
const safeAreaHeight = layout.round(layout.toDevicePixels(safeAreaSize.height));
if (hasChildControllers) {
// If not inner most extend to fullscreen
top = fullscreenOrigin.y;
height = fullscreenSize.height;
} else if (!scrollable) {
// If not scrollable dock under safe area
top = safeOrigin.y;
height = safeAreaSize.height;
} else if (navBarHidden) {
// If scrollable but no navigation bar dock under safe area
top = safeOrigin.y;
height = navController ? (fullscreenSize.height - top) : safeAreaSize.height;
} else {
// If scrollable and navigation bar extend to fullscreen
top = fullscreenOrigin.y;
height = fullscreenSize.height;
}
left = layout.toDevicePixels(left);
top = layout.toDevicePixels(top);
width = layout.toDevicePixels(width);
height = layout.toDevicePixels(height);
const widthSpec = layout.makeMeasureSpec(width, layout.EXACTLY);
const heightSpec = layout.makeMeasureSpec(height, layout.EXACTLY);
const widthSpec = layout.makeMeasureSpec(safeAreaWidth, layout.EXACTLY);
const heightSpec = layout.makeMeasureSpec(safeAreaHeight, layout.EXACTLY);
View.measureChild(null, owner, widthSpec, heightSpec);
View.layoutChild(null, owner, left, top, width + left, height + top);
View.layoutChild(null, owner, position.left, position.top, position.right, position.bottom);
layoutParent(owner.parent);
}
export function getPositionFromFrame(frame: CGRect): { left, top, right, bottom } {
const left = layout.round(layout.toDevicePixels(frame.origin.x));
const top = layout.round(layout.toDevicePixels(frame.origin.y));
const right = layout.round(layout.toDevicePixels(frame.origin.x + frame.size.width));
const bottom = layout.round(layout.toDevicePixels(frame.origin.y + frame.size.height));
return { left, right, top, bottom };
}
export function getFrameFromPosition(position: { left, top, right, bottom }, insets?: { left, top, right, bottom }): CGRect {
insets = insets || { left: 0, top: 0, right: 0, bottom: 0 };
const left = layout.toDeviceIndependentPixels(position.left + insets.left);
const top = layout.toDeviceIndependentPixels(position.top + insets.top);
const width = layout.toDeviceIndependentPixels(position.right - position.left - insets.left - insets.right);
const height = layout.toDeviceIndependentPixels(position.bottom - position.top - insets.top - insets.bottom);
return CGRectMake(left, top, width, height);
}
export function shrinkToSafeArea(view: View, frame: CGRect): CGRect {
const insets = view.getSafeAreaInsets();
if (insets.left || insets.top) {
const position = ios.getPositionFromFrame(frame);
const adjustedFrame = ios.getFrameFromPosition(position, insets);
if (traceEnabled()) {
traceWrite(this + " :shrinkToSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout);
}
return adjustedFrame;
}
return null;
}
export function expandBeyondSafeArea(view: View, frame: CGRect): CGRect {
const locationInWindow = view.getLocationInWindow();
const inWindowLeft = layout.round(layout.toDevicePixels(locationInWindow.x));
const inWindowTop = layout.round(layout.toDevicePixels(locationInWindow.y));
const inWindowRight = inWindowLeft + layout.round(layout.toDevicePixels(frame.size.width));
const inWindowBottom = inWindowTop + layout.round(layout.toDevicePixels(frame.size.height));
const availableSpace = getAvailableSpaceFromParent(view);
const safeArea = availableSpace.safeArea;
const fullscreen = availableSpace.fullscreen;
const position = ios.getPositionFromFrame(frame);
const safeAreaPosition = ios.getPositionFromFrame(safeArea);
const fullscreenPosition = ios.getPositionFromFrame(fullscreen);
const adjustedPosition = position;
if (position.left && inWindowLeft <= safeAreaPosition.left) {
adjustedPosition.left = fullscreenPosition.left;
}
if (position.top && inWindowTop <= safeAreaPosition.top) {
adjustedPosition.top = fullscreenPosition.top;
}
if (inWindowRight < fullscreenPosition.right && inWindowRight >= safeAreaPosition.right + fullscreenPosition.left) {
adjustedPosition.right = fullscreenPosition.right - fullscreenPosition.left;
}
if (inWindowBottom < fullscreenPosition.bottom && inWindowBottom >= safeAreaPosition.bottom + fullscreenPosition.top) {
adjustedPosition.bottom = fullscreenPosition.bottom - fullscreenPosition.top;
}
const adjustedFrame = CGRectMake(layout.toDeviceIndependentPixels(adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.top), layout.toDeviceIndependentPixels(adjustedPosition.right - adjustedPosition.left), layout.toDeviceIndependentPixels(adjustedPosition.bottom - adjustedPosition.top));
if (traceEnabled()) {
traceWrite(view + " :expandBeyondSafeArea: " + JSON.stringify(ios.getPositionFromFrame(adjustedFrame)), traceCategories.Layout);
}
return adjustedFrame;
}
function layoutParent(view: ViewBase): void {
if (!view) {
return;
@@ -749,6 +836,38 @@ export namespace ios {
layoutParent(view.parent);
}
function getAvailableSpaceFromParent(view: View): { safeArea: CGRect, fullscreen: CGRect } {
if (!view) {
return;
}
let fullscreen = null;
let safeArea = null;
if (view.viewController) {
const nativeView = view.viewController.view;
safeArea = nativeView.safeAreaLayoutGuide.layoutFrame;
fullscreen = nativeView.frame;
} else {
let parent = view.parent as View;
while (parent && !parent.viewController && !(parent.nativeViewProtected instanceof UIScrollView)) {
parent = parent.parent as View;
}
if (parent.nativeViewProtected instanceof UIScrollView) {
const nativeView = parent.nativeViewProtected;
safeArea = nativeView.safeAreaLayoutGuide.layoutFrame;
fullscreen = CGRectMake(0, 0, nativeView.contentSize.width, nativeView.contentSize.height);
} else if (parent.viewController) {
const nativeView = parent.viewController.view;
safeArea = nativeView.safeAreaLayoutGuide.layoutFrame;
fullscreen = nativeView.frame;
}
}
return { safeArea: safeArea, fullscreen: fullscreen }
}
export class UILayoutViewController extends UIViewController {
public owner: WeakRef<View>;
@@ -796,4 +915,4 @@ export namespace ios {
}
}
}
}
}

View File

@@ -47,13 +47,14 @@ export class AbsoluteLayout extends AbsoluteLayoutBase {
public onLayout(left: number, top: number, right: number, bottom: number): void {
super.onLayout(left, top, right, bottom);
const insets = this.getSafeAreaInsets();
this.eachLayoutChild((child, last) => {
const childWidth = child.getMeasuredWidth();
const childHeight = child.getMeasuredHeight();
const childLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + child.effectiveLeft;
const childTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + child.effectiveTop;
const childLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + child.effectiveLeft + insets.left;
const childTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + child.effectiveTop + insets.top;
const childRight = childLeft + childWidth + child.effectiveMarginLeft + child.effectiveMarginRight;
const childBottom = childTop + childHeight + child.effectiveMarginTop + child.effectiveMarginBottom;

View File

@@ -22,7 +22,7 @@ export class DockLayout extends DockLayoutBase {
const horizontalPaddingsAndMargins = this.effectivePaddingLeft + this.effectivePaddingRight + this.effectiveBorderLeftWidth + this.effectiveBorderRightWidth;
const verticalPaddingsAndMargins = this.effectivePaddingTop + this.effectivePaddingBottom + this.effectiveBorderTopWidth + this.effectiveBorderBottomWidth;
let remainingWidth = widthMode === layout.UNSPECIFIED ? Number.MAX_VALUE : width - horizontalPaddingsAndMargins;
let remainingHeight = heightMode === layout.UNSPECIFIED ? Number.MAX_VALUE : height - verticalPaddingsAndMargins;
@@ -79,11 +79,12 @@ export class DockLayout extends DockLayoutBase {
public onLayout(left: number, top: number, right: number, bottom: number): void {
super.onLayout(left, top, right, bottom);
const horizontalPaddingsAndMargins = this.effectivePaddingLeft + this.effectivePaddingRight + this.effectiveBorderLeftWidth + this.effectiveBorderRightWidth;
const verticalPaddingsAndMargins = this.effectivePaddingTop + this.effectivePaddingBottom + this.effectiveBorderTopWidth + this.effectiveBorderBottomWidth;
const insets = this.getSafeAreaInsets();
const horizontalPaddingsAndMargins = this.effectivePaddingLeft + this.effectivePaddingRight + this.effectiveBorderLeftWidth + this.effectiveBorderRightWidth + insets.left + insets.right;
const verticalPaddingsAndMargins = this.effectivePaddingTop + this.effectivePaddingBottom + this.effectiveBorderTopWidth + this.effectiveBorderBottomWidth + insets.top + insets.bottom;
let childLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft;
let childTop = this.effectiveBorderTopWidth + this.effectivePaddingTop;
let childLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + insets.left;
let childTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + insets.top;
let x = childLeft;
let y = childTop;

View File

@@ -943,38 +943,43 @@ export class FlexboxLayout extends FlexboxLayoutBase {
}
public onLayout(left: number, top: number, right: number, bottom: number) {
const insets = this.getSafeAreaInsets();
let isRtl;
switch (this.flexDirection) {
case FlexDirection.ROW:
isRtl = false;
this._layoutHorizontal(isRtl, left, top, right, bottom);
this._layoutHorizontal(isRtl, left, top, right, bottom, insets);
break;
case FlexDirection.ROW_REVERSE:
isRtl = true;
this._layoutHorizontal(isRtl, left, top, right, bottom);
this._layoutHorizontal(isRtl, left, top, right, bottom, insets);
break;
case FlexDirection.COLUMN:
isRtl = false;
if (this.flexWrap === FlexWrap.WRAP_REVERSE) {
isRtl = !isRtl;
}
this._layoutVertical(isRtl, false, left, top, right, bottom);
this._layoutVertical(isRtl, false, left, top, right, bottom, insets);
break;
case FlexDirection.COLUMN_REVERSE:
isRtl = false;
if (this.flexWrap === FlexWrap.WRAP_REVERSE) {
isRtl = !isRtl;
}
this._layoutVertical(isRtl, true, left, top, right, bottom);
this._layoutVertical(isRtl, true, left, top, right, bottom, insets);
break;
default:
throw new Error("Invalid flex direction is set: " + this.flexDirection);
}
}
private _layoutHorizontal(isRtl: boolean, left: number, top: number, right: number, bottom: number) {
let paddingLeft = this.effectivePaddingLeft;
let paddingRight = this.effectivePaddingRight;
private _layoutHorizontal(isRtl: boolean, left: number, top: number, right: number, bottom: number, insets: { left, top, right, bottom }) {
// include insets
let paddingLeft = this.effectivePaddingLeft + insets.left;
let paddingTop = this.effectivePaddingTop + insets.top;
let paddingRight = this.effectivePaddingRight + insets.right;
let paddingBottom = this.effectivePaddingBottom + insets.bottom;
let childLeft;
let currentViewIndex = 0;
@@ -982,8 +987,9 @@ export class FlexboxLayout extends FlexboxLayoutBase {
let height = bottom - top;
let width = right - left;
let childBottom = height - this.effectivePaddingBottom;
let childTop = this.effectivePaddingTop;
// include insets
let childBottom = height - paddingBottom;
let childTop = paddingTop;
let childRight;
this._flexLines.forEach((flexLine, i) => {
@@ -997,16 +1003,16 @@ export class FlexboxLayout extends FlexboxLayoutBase {
childRight = width - paddingRight;
break;
case JustifyContent.FLEX_END:
childLeft = width - flexLine._mainSize + paddingRight;
childRight = flexLine._mainSize - paddingLeft;
childLeft = width - flexLine._mainSize - paddingRight;
childRight = flexLine._mainSize + paddingLeft;
break;
case JustifyContent.CENTER:
childLeft = paddingLeft + (width - flexLine._mainSize) / 2.0;
childRight = width - paddingRight - (width - flexLine._mainSize) / 2.0;
childLeft = paddingLeft + (width - insets.left - insets.right - flexLine._mainSize) / 2.0;
childRight = width - paddingRight - (width - insets.left - insets.right - flexLine._mainSize) / 2.0;
break;
case JustifyContent.SPACE_AROUND:
if (flexLine._itemCount !== 0) {
spaceBetweenItem = (width - flexLine.mainSize) / flexLine._itemCount;
spaceBetweenItem = (width - insets.left - insets.right - flexLine.mainSize) / flexLine._itemCount;
}
childLeft = paddingLeft + spaceBetweenItem / 2.0;
childRight = width - paddingRight - spaceBetweenItem / 2.0;
@@ -1014,7 +1020,7 @@ export class FlexboxLayout extends FlexboxLayoutBase {
case JustifyContent.SPACE_BETWEEN:
childLeft = paddingLeft;
let denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0;
spaceBetweenItem = (width - flexLine.mainSize) / denominator;
spaceBetweenItem = (width - insets.left - insets.right - flexLine.mainSize) / denominator;
childRight = width - paddingRight;
break;
default:
@@ -1130,12 +1136,13 @@ export class FlexboxLayout extends FlexboxLayoutBase {
}
}
private _layoutVertical(isRtl: boolean, fromBottomToTop: boolean, left: number, top: number, right: number, bottom: number) {
let paddingTop = this.effectivePaddingTop;
let paddingBottom = this.effectivePaddingBottom;
private _layoutVertical(isRtl: boolean, fromBottomToTop: boolean, left: number, top: number, right: number, bottom: number, insets: { left, top, right, bottom }) {
let paddingLeft = this.effectivePaddingLeft + insets.left;
let paddingTop = this.effectivePaddingTop + insets.top;
let paddingRight = this.effectivePaddingRight + insets.right;
let paddingBottom = this.effectivePaddingBottom + insets.bottom;
let paddingRight = this.effectivePaddingRight;
let childLeft = this.effectivePaddingLeft;
let childLeft = paddingLeft;
let currentViewIndex = 0;
let width = right - left;
@@ -1157,16 +1164,16 @@ export class FlexboxLayout extends FlexboxLayoutBase {
childBottom = height - paddingBottom;
break;
case JustifyContent.FLEX_END:
childTop = height - flexLine._mainSize + paddingBottom;
childBottom = flexLine._mainSize - paddingTop;
childTop = height - flexLine._mainSize - paddingBottom;
childBottom = flexLine._mainSize + paddingTop;
break;
case JustifyContent.CENTER:
childTop = paddingTop + (height - flexLine._mainSize) / 2.0;
childBottom = height - paddingBottom - (height - flexLine._mainSize) / 2.0;
childTop = paddingTop + (height - insets.top - insets.bottom - flexLine._mainSize) / 2.0;
childBottom = height - paddingBottom - (height - insets.top - insets.bottom - flexLine._mainSize) / 2.0;
break;
case JustifyContent.SPACE_AROUND:
if (flexLine._itemCount !== 0) {
spaceBetweenItem = (height - flexLine._mainSize) / flexLine.itemCount;
spaceBetweenItem = (height - insets.top - insets.bottom - flexLine._mainSize) / flexLine.itemCount;
}
childTop = paddingTop + spaceBetweenItem / 2.0;
childBottom = height - paddingBottom - spaceBetweenItem / 2.0;
@@ -1174,7 +1181,7 @@ export class FlexboxLayout extends FlexboxLayoutBase {
case JustifyContent.SPACE_BETWEEN:
childTop = paddingTop;
let denominator = flexLine.itemCount !== 1 ? flexLine.itemCount - 1 : 1.0;
spaceBetweenItem = (height - flexLine.mainSize) / denominator;
spaceBetweenItem = (height - insets.top - insets.bottom - flexLine.mainSize) / denominator;
childBottom = height - paddingBottom;
break;
default:

View File

@@ -159,8 +159,10 @@ export class GridLayout extends GridLayoutBase {
public onLayout(left: number, top: number, right: number, bottom: number): void {
super.onLayout(left, top, right, bottom);
let paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft;
let paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop;
const insets = this.getSafeAreaInsets();
let paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + insets.left;
let paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + insets.top;
this.columnOffsets.length = 0;
this.rowOffsets.length = 0;

View File

@@ -1,12 +1,12 @@
import {
LayoutBaseCommon, clipToBoundsProperty, isPassThroughParentEnabledProperty, View
import {
LayoutBaseCommon, clipToBoundsProperty, isPassThroughParentEnabledProperty, View
} from "./layout-base-common";
export * from "./layout-base-common";
export class LayoutBase extends LayoutBaseCommon {
nativeViewProtected: UIView;
public addChild(child: View): void {
super.addChild(child);
this.requestLayout();
@@ -29,7 +29,7 @@ export class LayoutBase extends LayoutBaseCommon {
super._setNativeClipToBounds();
}
}
[clipToBoundsProperty.getDefault](): boolean {
return false;
}

View File

@@ -83,19 +83,21 @@ export class StackLayout extends StackLayoutBase {
public onLayout(left: number, top: number, right: number, bottom: number): void {
super.onLayout(left, top, right, bottom);
const insets = this.getSafeAreaInsets();
if (this.orientation === "vertical") {
this.layoutVertical(left, top, right, bottom);
this.layoutVertical(left, top, right, bottom, insets);
}
else {
this.layoutHorizontal(left, top, right, bottom);
this.layoutHorizontal(left, top, right, bottom, insets);
}
}
private layoutVertical(left: number, top: number, right: number, bottom: number): void {
const paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft;
const paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop;
const paddingRight = this.effectiveBorderRightWidth + this.effectivePaddingRight;
const paddingBottom = this.effectiveBorderBottomWidth + this.effectivePaddingBottom;
private layoutVertical(left: number, top: number, right: number, bottom: number, insets: {left, top, right, bottom}): void {
const paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + insets.left;
const paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + insets.top;
const paddingRight = this.effectiveBorderRightWidth + this.effectivePaddingRight + insets.right;
const paddingBottom = this.effectiveBorderBottomWidth + this.effectivePaddingBottom + insets.bottom;
let childTop: number;
let childLeft: number = paddingLeft;
@@ -125,11 +127,11 @@ export class StackLayout extends StackLayoutBase {
})
}
private layoutHorizontal(left: number, top: number, right: number, bottom: number): void {
const paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft;
const paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop;
const paddingRight = this.effectiveBorderRightWidth + this.effectivePaddingRight;
const paddingBottom = this.effectiveBorderBottomWidth + this.effectivePaddingBottom;
private layoutHorizontal(left: number, top: number, right: number, bottom: number, insets: {left, top, right, bottom}): void {
const paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + insets.left;
const paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + insets.top;
const paddingRight = this.effectiveBorderRightWidth + this.effectivePaddingRight + insets.right;
const paddingBottom = this.effectiveBorderBottomWidth + this.effectivePaddingBottom + insets.bottom;
let childTop: number = paddingTop;
let childLeft: number;

View File

@@ -122,35 +122,31 @@ export class WrapLayout extends WrapLayoutBase {
public onLayout(left: number, top: number, right: number, bottom: number): void {
super.onLayout(left, top, right, bottom);
const insets = this.getSafeAreaInsets();
const isVertical = this.orientation === "vertical";
const paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft;
const paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop;
const paddingRight = this.effectiveBorderRightWidth + this.effectivePaddingRight;
const paddingBottom = this.effectiveBorderBottomWidth + this.effectivePaddingBottom;
const paddingLeft = this.effectiveBorderLeftWidth + this.effectivePaddingLeft + insets.left;
const paddingTop = this.effectiveBorderTopWidth + this.effectivePaddingTop + insets.top;
const paddingRight = this.effectiveBorderRightWidth + this.effectivePaddingRight + insets.right;
const paddingBottom = this.effectiveBorderBottomWidth + this.effectivePaddingBottom + insets.bottom;
let childLeft = paddingLeft;
let childTop = paddingTop;
let childrenLength: number;
if (isVertical) {
childrenLength = bottom - top - paddingBottom;
}
else {
childrenLength = right - left - paddingRight;
}
let childrenHeight = bottom - top - paddingBottom;
let childrenWidth = right - left - paddingRight;
let rowOrColumn = 0;
var rowOrColumn = 0;
this.eachLayoutChild((child, last) => {
// Add margins because layoutChild will sustract them.
// * density converts them to device pixels.
let childHeight = child.getMeasuredHeight() + child.effectiveMarginTop + child.effectiveMarginBottom;
let childWidth = child.getMeasuredWidth() + child.effectiveMarginLeft + child.effectiveMarginRight;
let length = this._lengths[rowOrColumn];
if (isVertical) {
childWidth = length;
childHeight = this.effectiveItemHeight > 0 ? this.effectiveItemHeight : childHeight;
let isFirst = childTop === paddingTop;
if (childTop + childHeight > childrenLength) {
if (childTop + childHeight > childrenHeight && childLeft + childWidth <= childrenWidth) {
// Move to top.
childTop = paddingTop;
@@ -165,12 +161,18 @@ export class WrapLayout extends WrapLayoutBase {
// Take respective column width.
childWidth = this._lengths[isFirst ? rowOrColumn - 1 : rowOrColumn];
}
}
else {
if (childLeft < childrenWidth && childTop < childrenHeight) {
View.layoutChild(this, child, childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
// Move next child Top position to bottom.
childTop += childHeight;
} else {
childWidth = this.effectiveItemWidth > 0 ? this.effectiveItemWidth : childWidth;
childHeight = length;
let isFirst = childLeft === paddingLeft;
if (childLeft + childWidth > childrenLength) {
if (childLeft + childWidth > childrenWidth && childTop + childHeight <= childrenHeight) {
// Move to left.
childLeft = paddingLeft;
@@ -185,15 +187,11 @@ export class WrapLayout extends WrapLayoutBase {
// Take respective row height.
childHeight = this._lengths[isFirst ? rowOrColumn - 1 : rowOrColumn];
}
}
View.layoutChild(this, child, childLeft, childTop, childLeft + childWidth, childTop + childHeight);
if (childLeft < childrenWidth && childTop < childrenHeight) {
View.layoutChild(this, child, childLeft, childTop, childLeft + childWidth, childTop + childHeight);
}
if (isVertical) {
// Move next child Top position to bottom.
childTop += childHeight;
}
else {
// Move next child Left position to right.
childLeft += childWidth;
}

View File

@@ -1,5 +1,5 @@
import { ListView as ListViewDefinition, ItemsSource, ItemEventData, TemplatedItemsView } from ".";
import { CoercibleProperty, CssProperty, Style, View, Template, KeyedTemplate, Length, Property, Color, Observable, EventData, CSSType } from "../core/view";
import { CoercibleProperty, CssProperty, Style, View, ViewBase, ContainerView, Template, KeyedTemplate, Length, Property, Color, Observable, EventData, CSSType } from "../core/view";
import { parse, parseMultipleTemplates } from "../builder";
import { Label } from "../label";
import { ObservableArray, ChangedData } from "../../data/observable-array";
@@ -19,7 +19,7 @@ export module knownMultiTemplates {
const autoEffectiveRowHeight = -1;
@CSSType("ListView")
export abstract class ListViewBase extends View implements ListViewDefinition, TemplatedItemsView {
export abstract class ListViewBase extends ContainerView implements ListViewDefinition, TemplatedItemsView {
public static itemLoadingEvent = "itemLoading";
public static itemTapEvent = "itemTap";
public static loadMoreItemsEvent = "loadMoreItems";

View File

@@ -7,6 +7,7 @@ import { StackLayout } from "../layouts/stack-layout";
import { ProxyViewContainer } from "../proxy-view-container";
import { profile } from "../../profiling";
import * as trace from "../../trace";
import { ios as iosUtils } from "../../utils/utils";
export * from "./list-view-common";
@@ -22,6 +23,7 @@ interface ViewItemIndex {
}
type ItemView = View & ViewItemIndex;
const majorVersion = iosUtils.MajorVersion;
class ListViewCell extends UITableViewCell {
public static initWithEmptyBackground(): ListViewCell {

View File

@@ -120,7 +120,7 @@ class UIViewControllerImpl extends UIViewController {
// Skip navigation events if modal page is shown.
if (!owner._presentedViewController && frame) {
const newEntry = this[ENTRY];
let isBack: boolean;
// We are on the current page which happens when navigation is canceled so isBack should be false.
if (frame.currentPage === owner && frame._navigationQueue.length === 0) {
@@ -312,7 +312,14 @@ export class Page extends PageBase {
public onLayout(left: number, top: number, right: number, bottom: number) {
const { width: actionBarWidth, height: actionBarHeight } = this.actionBar._getActualSize;
View.layoutChild(this, this.actionBar, 0, 0, actionBarWidth, actionBarHeight);
View.layoutChild(this, this.layoutView, left, top, right, bottom);
const insets = this.getSafeAreaInsets();
const childLeft = 0 + insets.left;
const childTop = 0 + insets.top;
const childRight = right - left - insets.right;
const childBottom = bottom - top - insets.bottom;
View.layoutChild(this, this.layoutView, childLeft, childTop, childRight, childBottom);
}
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
@@ -330,7 +337,7 @@ export class Page extends PageBase {
if (this.viewController.presentedViewController === viewController) {
return true;
}
this.viewController.addChildViewController(viewController);
}

View File

@@ -101,7 +101,12 @@ export class Repeater extends CustomLayoutView implements RepeaterDefinition {
}
public onLayout(left: number, top: number, right: number, bottom: number): void {
View.layoutChild(this, this.itemsLayout, 0, 0, right - left, bottom - top);
const insets = this.getSafeAreaInsets();
const childLeft = left + insets.left;
const childTop = top + insets.top;
const childRight = right - insets.right;
const childBottom = bottom - insets.bottom;
View.layoutChild(this, this.itemsLayout, childLeft, childTop, childRight, childBottom);
}
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {

View File

@@ -132,15 +132,6 @@ export class ScrollView extends ScrollViewBase {
this._contentMeasuredWidth = this.effectiveMinWidth;
this._contentMeasuredHeight = this.effectiveMinHeight;
// `_automaticallyAdjustsScrollViewInsets` is set to true only if the first child
// of UIViewController (Page, TabView e.g) is UIScrollView (ScrollView, ListView e.g).
// On iOS 11 by default UIScrollView automatically adjusts the scroll view insets, but they s
if (majorVersion > 10 && !this.parent._automaticallyAdjustsScrollViewInsets) {
// Disable automatic adjustment of scroll view insets when ScrollView
// is not the first child of UIViewController.
this.nativeViewProtected.contentInsetAdjustmentBehavior = 2;
}
if (child) {
let childSize: { measuredWidth: number; measuredHeight: number };
if (this.orientation === "vertical") {
@@ -149,10 +140,6 @@ export class ScrollView extends ScrollViewBase {
childSize = View.measureChild(this, child, layout.makeMeasureSpec(0, layout.UNSPECIFIED), heightMeasureSpec);
}
const w = layout.toDeviceIndependentPixels(childSize.measuredWidth);
const h = layout.toDeviceIndependentPixels(childSize.measuredHeight);
this.nativeViewProtected.contentSize = CGSizeMake(w, h);
this._contentMeasuredWidth = Math.max(childSize.measuredWidth, this.effectiveMinWidth);
this._contentMeasuredHeight = Math.max(childSize.measuredHeight, this.effectiveMinHeight);
}
@@ -164,25 +151,34 @@ export class ScrollView extends ScrollViewBase {
}
public onLayout(left: number, top: number, right: number, bottom: number): void {
const width = (right - left);
const height = (bottom - top);
const insets = this.getSafeAreaInsets();
let width = (right - left - insets.right - insets.left);
let height = (bottom - top - insets.bottom - insets.top);
let verticalInset: number;
const nativeView = this.nativeViewProtected;
const inset = nativeView.adjustedContentInset;
// Prior iOS 11
if (inset === undefined) {
verticalInset = -layout.toDevicePixels(nativeView.contentOffset.y);
verticalInset += getTabBarHeight(this);
} else {
verticalInset = layout.toDevicePixels(inset.bottom + inset.top);
if (majorVersion > 10) {
// Disable automatic adjustment of scroll view insets
// Consider exposing this as property with all 4 modes
// https://developer.apple.com/documentation/uikit/uiscrollview/contentinsetadjustmentbehavior
nativeView.contentInsetAdjustmentBehavior = 2;
}
let scrollWidth = width;
let scrollHeight = height;
if (this.orientation === "horizontal") {
View.layoutChild(this, this.layoutView, 0, 0, Math.max(this._contentMeasuredWidth, width), height - verticalInset);
} else {
View.layoutChild(this, this.layoutView, 0, 0, width, Math.max(this._contentMeasuredHeight, height - verticalInset));
scrollWidth = Math.max(this._contentMeasuredWidth + insets.left + insets.right, width);
scrollHeight = height + insets.top + insets.bottom;
width = Math.max(this._contentMeasuredWidth, width);
}
else {
scrollHeight = Math.max(this._contentMeasuredHeight + insets.top + insets.bottom, height);
scrollWidth = width + insets.left + insets.right;
height = Math.max(this._contentMeasuredHeight, height);
}
nativeView.contentSize = CGSizeMake(layout.toDeviceIndependentPixels(scrollWidth), layout.toDeviceIndependentPixels(scrollHeight));
View.layoutChild(this, this.layoutView, insets.left, insets.top, insets.left + width, insets.top + height);
}
public _onOrientationChanged() {

View File

@@ -1,5 +1,5 @@
import { WebView as WebViewDefinition, LoadEventData, NavigationType } from ".";
import { View, Property, EventData, CSSType } from "../core/view";
import { ContainerView, Property, EventData, CSSType } from "../core/view";
import { File, knownFolders, path } from "../../file-system";
export { File, knownFolders, path, NavigationType };
@@ -8,7 +8,7 @@ export * from "../core/view";
export const srcProperty = new Property<WebViewBase, string>({ name: "src" });
@CSSType("WebView")
export abstract class WebViewBase extends View implements WebViewDefinition {
export abstract class WebViewBase extends ContainerView implements WebViewDefinition {
public static loadStartedEvent = "loadStarted";
public static loadFinishedEvent = "loadFinished";