mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-08-16 11:42:04 +08:00
465 lines
16 KiB
TypeScript
465 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 {
|
|
let view = this.nativeView;
|
|
let nativeWidth = 0;
|
|
let nativeHeight = 0;
|
|
|
|
let width = layout.getMeasureSpecSize(widthMeasureSpec);
|
|
let widthMode = layout.getMeasureSpecMode(widthMeasureSpec);
|
|
|
|
let height = layout.getMeasureSpecSize(heightMeasureSpec);
|
|
let heightMode = layout.getMeasureSpecMode(heightMeasureSpec);
|
|
|
|
if (view) {
|
|
if (widthMode === layout.UNSPECIFIED) {
|
|
width = Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
if (heightMode === layout.UNSPECIFIED) {
|
|
height = Number.POSITIVE_INFINITY;
|
|
}
|
|
|
|
let nativeSize = view.sizeThatFits(CGSizeMake(width, height));
|
|
nativeWidth = layout.toDevicePixels(nativeSize.width);
|
|
nativeHeight = layout.toDevicePixels(nativeSize.height);
|
|
}
|
|
|
|
let measureWidth = Math.max(nativeWidth, this.effectiveMinWidth);
|
|
let measureHeight = Math.max(nativeHeight, this.effectiveMinHeight);
|
|
|
|
let widthAndState = View.resolveSizeAndState(measureWidth, width, widthMode, 0);
|
|
let 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();
|
|
}
|
|
}
|
|
}
|