mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
* Layout round instead of cailing Add helper method to layout module to convert to/from dips to px and measure the native view whiteSpace affects layout added for iOS Fix bug in switch onMeasure implementation Fix bug in cssValueToDevicePixels iOS implementation ActionBar for iOS is measured with AT_MOST modifier * Fix switch measure routine
456 lines
16 KiB
TypeScript
456 lines
16 KiB
TypeScript
// Definitions.
|
|
import { Point, View as ViewDefinition } from ".";
|
|
|
|
import { ios, Background } from "../../styling/background";
|
|
import {
|
|
ViewCommon, layout, isEnabledProperty, originXProperty, originYProperty, automationTextProperty, isUserInteractionEnabledProperty,
|
|
traceEnabled, traceWrite, traceCategories
|
|
} from "./view-common";
|
|
|
|
import {
|
|
Visibility, Length,
|
|
visibilityProperty, opacityProperty,
|
|
rotateProperty, scaleXProperty, scaleYProperty,
|
|
translateXProperty, translateYProperty, zIndexProperty,
|
|
backgroundInternalProperty, clipPathProperty
|
|
} from "../../styling/style-properties";
|
|
|
|
export * from "./view-common";
|
|
|
|
const PFLAG_FORCE_LAYOUT = 1;
|
|
const PFLAG_MEASURED_DIMENSION_SET = 1 << 1;
|
|
const PFLAG_LAYOUT_REQUIRED = 1 << 2;
|
|
|
|
export class View extends ViewCommon {
|
|
private _hasTransfrom = false;
|
|
private _privateFlags: number = PFLAG_LAYOUT_REQUIRED | PFLAG_FORCE_LAYOUT;
|
|
private _cachedFrame: CGRect;
|
|
private _suspendCATransaction = false;
|
|
|
|
get _nativeView(): UIView {
|
|
return this.ios;
|
|
}
|
|
|
|
public _addViewCore(view: ViewCommon, atIndex?: number) {
|
|
super._addViewCore(view, atIndex);
|
|
this.requestLayout();
|
|
}
|
|
|
|
public _removeViewCore(view: ViewCommon) {
|
|
super._removeViewCore(view);
|
|
this.requestLayout();
|
|
}
|
|
|
|
get isLayoutRequired(): boolean {
|
|
return (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED;
|
|
}
|
|
|
|
get isLayoutRequested(): boolean {
|
|
return (this._privateFlags & PFLAG_FORCE_LAYOUT) === PFLAG_FORCE_LAYOUT;
|
|
}
|
|
|
|
public requestLayout(): void {
|
|
super.requestLayout();
|
|
this._privateFlags |= PFLAG_FORCE_LAYOUT;
|
|
|
|
let parent = <View>this.parent;
|
|
if (parent && !parent.isLayoutRequested) {
|
|
parent.requestLayout();
|
|
}
|
|
}
|
|
|
|
public measure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
|
let measureSpecsChanged = this._setCurrentMeasureSpecs(widthMeasureSpec, heightMeasureSpec);
|
|
let forceLayout = (this._privateFlags & PFLAG_FORCE_LAYOUT) === PFLAG_FORCE_LAYOUT;
|
|
if (forceLayout || measureSpecsChanged) {
|
|
|
|
// first clears the measured dimension flag
|
|
this._privateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
|
|
|
|
// measure ourselves, this should set the measured dimension flag back
|
|
this.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
|
this._privateFlags |= PFLAG_LAYOUT_REQUIRED;
|
|
|
|
// flag not set, setMeasuredDimension() was not invoked, we raise
|
|
// an exception to warn the developer
|
|
if ((this._privateFlags & PFLAG_MEASURED_DIMENSION_SET) !== PFLAG_MEASURED_DIMENSION_SET) {
|
|
throw new Error("onMeasure() did not set the measured dimension by calling setMeasuredDimension() " + this);
|
|
}
|
|
}
|
|
}
|
|
|
|
public layout(left: number, top: number, right: number, bottom: number): void {
|
|
let { boundsChanged, sizeChanged } = this._setCurrentLayoutBounds(left, top, right, bottom);
|
|
this.layoutNativeView(left, top, right, bottom);
|
|
if (boundsChanged || (this._privateFlags & PFLAG_LAYOUT_REQUIRED) === PFLAG_LAYOUT_REQUIRED) {
|
|
this.onLayout(left, top, right, bottom);
|
|
this._privateFlags &= ~PFLAG_LAYOUT_REQUIRED;
|
|
}
|
|
|
|
if (sizeChanged) {
|
|
this._onSizeChanged();
|
|
}
|
|
|
|
this._privateFlags &= ~PFLAG_FORCE_LAYOUT;
|
|
}
|
|
|
|
public setMeasuredDimension(measuredWidth: number, measuredHeight: number): void {
|
|
super.setMeasuredDimension(measuredWidth, measuredHeight);
|
|
this._privateFlags |= PFLAG_MEASURED_DIMENSION_SET;
|
|
}
|
|
|
|
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
|
const view = this.nativeView;
|
|
const width = layout.getMeasureSpecSize(widthMeasureSpec);
|
|
const widthMode = layout.getMeasureSpecMode(widthMeasureSpec);
|
|
|
|
const height = layout.getMeasureSpecSize(heightMeasureSpec);
|
|
const heightMode = layout.getMeasureSpecMode(heightMeasureSpec);
|
|
|
|
let nativeWidth = 0;
|
|
let nativeHeight = 0;
|
|
if (view) {
|
|
const nativeSize = layout.measureNativeView(view, width, widthMode, height, heightMode);
|
|
nativeWidth = nativeSize.width;
|
|
nativeHeight = nativeSize.height;
|
|
}
|
|
|
|
const measureWidth = Math.max(nativeWidth, this.effectiveMinWidth);
|
|
const measureHeight = Math.max(nativeHeight, this.effectiveMinHeight);
|
|
|
|
const widthAndState = View.resolveSizeAndState(measureWidth, width, widthMode, 0);
|
|
const heightAndState = View.resolveSizeAndState(measureHeight, height, heightMode, 0);
|
|
|
|
this.setMeasuredDimension(widthAndState, heightAndState);
|
|
}
|
|
|
|
public onLayout(left: number, top: number, right: number, bottom: number): void {
|
|
//
|
|
}
|
|
|
|
public _setNativeViewFrame(nativeView: UIView, frame: CGRect) {
|
|
if (!CGRectEqualToRect(nativeView.frame, frame)) {
|
|
if (traceEnabled()) {
|
|
traceWrite(this + ", Native setFrame: = " + NSStringFromCGRect(frame), traceCategories.Layout);
|
|
}
|
|
this._cachedFrame = frame;
|
|
if (this._hasTransfrom) {
|
|
// Always set identity transform before setting frame;
|
|
let transform = nativeView.transform;
|
|
nativeView.transform = CGAffineTransformIdentity;
|
|
nativeView.frame = frame;
|
|
nativeView.transform = transform;
|
|
}
|
|
else {
|
|
nativeView.frame = frame;
|
|
}
|
|
let boundsOrigin = nativeView.bounds.origin;
|
|
nativeView.bounds = CGRectMake(boundsOrigin.x, boundsOrigin.y, frame.size.width, frame.size.height);
|
|
}
|
|
}
|
|
|
|
public layoutNativeView(left: number, top: number, right: number, bottom: number): void {
|
|
if (!this.nativeView) {
|
|
return;
|
|
}
|
|
|
|
let nativeView = this.nativeView;
|
|
|
|
let frame = CGRectMake(layout.toDeviceIndependentPixels(left), layout.toDeviceIndependentPixels(top), layout.toDeviceIndependentPixels(right - left), layout.toDeviceIndependentPixels(bottom - top));
|
|
this._setNativeViewFrame(nativeView, frame);
|
|
}
|
|
|
|
public _updateLayout() {
|
|
let oldBounds = this._getCurrentLayoutBounds();
|
|
this.layoutNativeView(oldBounds.left, oldBounds.top, oldBounds.right, oldBounds.bottom);
|
|
}
|
|
|
|
public focus(): boolean {
|
|
if (this.ios) {
|
|
return this.ios.becomeFirstResponder();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public getLocationInWindow(): Point {
|
|
if (!this.nativeView || !this.nativeView.window) {
|
|
return undefined;
|
|
}
|
|
|
|
let pointInWindow = this.nativeView.convertPointToView(this.nativeView.bounds.origin, null);
|
|
return {
|
|
x: pointInWindow.x,
|
|
y: pointInWindow.y
|
|
};
|
|
}
|
|
|
|
public getLocationOnScreen(): Point {
|
|
if (!this.nativeView || !this.nativeView.window) {
|
|
return undefined;
|
|
}
|
|
|
|
let pointInWindow = this.nativeView.convertPointToView(this.nativeView.bounds.origin, null);
|
|
let pointOnScreen = this.nativeView.window.convertPointToWindow(pointInWindow, null);
|
|
return {
|
|
x: pointOnScreen.x,
|
|
y: pointOnScreen.y
|
|
};
|
|
}
|
|
|
|
public getLocationRelativeTo(otherView: ViewDefinition): Point {
|
|
if (!this.nativeView || !this.nativeView.window ||
|
|
!otherView.nativeView || !otherView.nativeView.window ||
|
|
this.nativeView.window !== otherView.nativeView.window) {
|
|
return undefined;
|
|
}
|
|
|
|
let myPointInWindow = this.nativeView.convertPointToView(this.nativeView.bounds.origin, null);
|
|
let otherPointInWindow = otherView.nativeView.convertPointToView(otherView.nativeView.bounds.origin, null);
|
|
return {
|
|
x: myPointInWindow.x - otherPointInWindow.x,
|
|
y: myPointInWindow.y - otherPointInWindow.y
|
|
};
|
|
}
|
|
|
|
private _onSizeChanged(): void {
|
|
let nativeView = this.nativeView;
|
|
if (!nativeView) {
|
|
return;
|
|
}
|
|
|
|
let background = this.style.backgroundInternal;
|
|
if (!background.isEmpty()) {
|
|
this[backgroundInternalProperty.native] = background;
|
|
}
|
|
|
|
let clipPath = this.style.clipPath;
|
|
if (clipPath !== "") {
|
|
this[clipPathProperty.native] = clipPath;
|
|
}
|
|
}
|
|
|
|
public updateNativeTransform() {
|
|
let translateX = Length.toDevicePixels(this.translateX || 0, 0);
|
|
let translateY = Length.toDevicePixels(this.translateY || 0, 0);
|
|
let scaleX = this.scaleX || 1;
|
|
let scaleY = this.scaleY || 1;
|
|
let rotate = this.rotate || 0;
|
|
let newTransform = CGAffineTransformIdentity;
|
|
newTransform = CGAffineTransformTranslate(newTransform, translateX, translateY);
|
|
newTransform = CGAffineTransformRotate(newTransform, rotate * Math.PI / 180);
|
|
newTransform = CGAffineTransformScale(newTransform, scaleX === 0 ? 0.001 : scaleX, scaleY === 0 ? 0.001 : scaleY);
|
|
if (!CGAffineTransformEqualToTransform(this.nativeView.transform, newTransform)) {
|
|
this.nativeView.transform = newTransform;
|
|
this._hasTransfrom = this.nativeView && !CGAffineTransformEqualToTransform(this.nativeView.transform, CGAffineTransformIdentity);
|
|
}
|
|
}
|
|
|
|
public updateOriginPoint(originX: number, originY: number) {
|
|
let newPoint = CGPointMake(originX, originY);
|
|
this.nativeView.layer.anchorPoint = newPoint;
|
|
if (this._cachedFrame) {
|
|
this._setNativeViewFrame(this.nativeView, this._cachedFrame);
|
|
}
|
|
}
|
|
|
|
// By default we update the view's presentation layer when setting backgroundColor and opacity properties.
|
|
// This is done by calling CATransaction begin and commit methods.
|
|
// This action should be disabled when updating those properties during an animation.
|
|
public _suspendPresentationLayerUpdates() {
|
|
this._suspendCATransaction = true;
|
|
}
|
|
|
|
public _resumePresentationLayerUpdates() {
|
|
this._suspendCATransaction = false;
|
|
}
|
|
|
|
public _isPresentationLayerUpdateSuspeneded() {
|
|
return this._suspendCATransaction;
|
|
}
|
|
|
|
get [isEnabledProperty.native](): boolean {
|
|
let nativeView = this.nativeView;
|
|
return nativeView instanceof UIControl ? nativeView.enabled : true;
|
|
}
|
|
set [isEnabledProperty.native](value: boolean) {
|
|
let nativeView = this.nativeView;
|
|
if (nativeView instanceof UIControl) {
|
|
nativeView.enabled = value;
|
|
}
|
|
}
|
|
|
|
get [originXProperty.native](): number {
|
|
return this.nativeView.layer.anchorPoint.x;
|
|
}
|
|
set [originXProperty.native](value: number) {
|
|
this.updateOriginPoint(value, this.originY);
|
|
}
|
|
|
|
get [originYProperty.native](): number {
|
|
return this.nativeView.layer.anchorPoint.y;
|
|
}
|
|
set [originYProperty.native](value: number) {
|
|
this.updateOriginPoint(this.originX, value);
|
|
}
|
|
|
|
get [automationTextProperty.native](): string {
|
|
return this.nativeView.accessibilityLabel;
|
|
}
|
|
set [automationTextProperty.native](value: string) {
|
|
this.nativeView.accessibilityIdentifier = value;
|
|
this.nativeView.accessibilityLabel = value;
|
|
}
|
|
|
|
get [isUserInteractionEnabledProperty.native](): boolean {
|
|
return this.nativeView.userInteractionEnabled;
|
|
}
|
|
set [isUserInteractionEnabledProperty.native](value: boolean) {
|
|
this.nativeView.userInteractionEnabled = value;
|
|
}
|
|
|
|
get [visibilityProperty.native](): Visibility {
|
|
return this.nativeView.hidden ? Visibility.COLLAPSE : Visibility.VISIBLE;
|
|
}
|
|
set [visibilityProperty.native](value: Visibility) {
|
|
switch (value) {
|
|
case Visibility.VISIBLE:
|
|
this.nativeView.hidden = false;
|
|
break;
|
|
case Visibility.HIDDEN:
|
|
case Visibility.COLLAPSE:
|
|
this.nativeView.hidden = true;
|
|
break;
|
|
default:
|
|
throw new Error(`Invalid visibility value: ${value}. Valid values are: "${Visibility.VISIBLE}", "${Visibility.HIDDEN}", "${Visibility.COLLAPSE}".`);
|
|
}
|
|
}
|
|
|
|
get [opacityProperty.native](): number {
|
|
return this.nativeView.alpha;
|
|
}
|
|
set [opacityProperty.native](value: number) {
|
|
let nativeView = this.nativeView;
|
|
let updateSuspended = this._isPresentationLayerUpdateSuspeneded();
|
|
if (!updateSuspended) {
|
|
CATransaction.begin();
|
|
}
|
|
nativeView.alpha = value;
|
|
if (!updateSuspended) {
|
|
CATransaction.commit();
|
|
}
|
|
}
|
|
|
|
get [rotateProperty.native](): number {
|
|
return 0;
|
|
}
|
|
set [rotateProperty.native](value: number) {
|
|
this.updateNativeTransform();
|
|
}
|
|
|
|
get [scaleXProperty.native](): number {
|
|
return 1;
|
|
}
|
|
set [scaleXProperty.native](value: number) {
|
|
this.updateNativeTransform();
|
|
}
|
|
|
|
get [scaleYProperty.native](): number {
|
|
return 1;
|
|
}
|
|
set [scaleYProperty.native](value: number) {
|
|
this.updateNativeTransform();
|
|
}
|
|
|
|
get [translateXProperty.native](): Length | number {
|
|
return 0;
|
|
}
|
|
set [translateXProperty.native](value: Length) {
|
|
this.updateNativeTransform();
|
|
}
|
|
|
|
get [translateYProperty.native](): Length | number {
|
|
return 0;
|
|
}
|
|
set [translateYProperty.native](value: Length) {
|
|
this.updateNativeTransform();
|
|
}
|
|
|
|
get [zIndexProperty.native](): number {
|
|
return 0;
|
|
}
|
|
set [zIndexProperty.native](value: number) {
|
|
this.nativeView.layer.zPosition = value;
|
|
}
|
|
|
|
get [backgroundInternalProperty.native](): UIColor {
|
|
return this.nativeView.backgroundColor;
|
|
}
|
|
set [backgroundInternalProperty.native](value: UIColor | Background) {
|
|
let updateSuspended = this._isPresentationLayerUpdateSuspeneded();
|
|
if (!updateSuspended) {
|
|
CATransaction.begin();
|
|
}
|
|
|
|
if (value instanceof UIColor) {
|
|
this.nativeView.backgroundColor = value;
|
|
} else {
|
|
this.nativeView.backgroundColor = ios.createBackgroundUIColor(this);
|
|
this._setNativeClipToBounds();
|
|
}
|
|
if (!updateSuspended) {
|
|
CATransaction.commit();
|
|
}
|
|
}
|
|
|
|
_setNativeClipToBounds() {
|
|
let backgroundInternal = this.style.backgroundInternal;
|
|
this.nativeView.clipsToBounds = backgroundInternal.hasBorderWidth() || backgroundInternal.hasBorderRadius();
|
|
}
|
|
}
|
|
|
|
export class CustomLayoutView extends View {
|
|
|
|
private _view: UIView;
|
|
|
|
constructor() {
|
|
super();
|
|
this._view = UIView.new();
|
|
}
|
|
|
|
get ios(): UIView {
|
|
return this._view;
|
|
}
|
|
|
|
public onMeasure(widthMeasureSpec: number, heightMeasureSpec: number): void {
|
|
// Don't call super because it will set MeasureDimension. This method must be overriden and calculate its measuredDimensions.
|
|
}
|
|
|
|
public _addViewToNativeVisualTree(child: View, atIndex: number): boolean {
|
|
super._addViewToNativeVisualTree(child, atIndex);
|
|
|
|
const parentNativeView = this.nativeView;
|
|
const childNativeView = child.nativeView;
|
|
|
|
if (parentNativeView && childNativeView) {
|
|
if (typeof atIndex !== "number" || atIndex >= parentNativeView.subviews.count) {
|
|
parentNativeView.addSubview(childNativeView);
|
|
} else {
|
|
parentNativeView.insertSubviewAtIndex(childNativeView, atIndex);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public _removeViewFromNativeVisualTree(child: View): void {
|
|
super._removeViewFromNativeVisualTree(child);
|
|
|
|
if (child.nativeView) {
|
|
child.nativeView.removeFromSuperview();
|
|
}
|
|
}
|
|
}
|