NativeScriptは、有名なJavaScriptのObject.definePropertyのラッパーに基づく独自のプロパティシステムを提供します。 UIおよびCSS要素を使用したモバイル開発のコンテキストで優れた開発利便性を提供するために、Propertyクラスの拡張クラスを提供しました。 この記事では、提供されたプロパティクラスと、初期化、登録、ビューライフサイクル、変更されたイベントのリサイクルと処理など、ビューとプロパティを操作する際の基本テクニックについて説明します。 Viewの一般的に使用されるいくつかの方法も示されています。
すべてのプロパティクラスの実装は、tns-core-modules/ui/core/propertiesモジュールの下にあります。 以下では、そのモジュールから公開されているすべてのクラスを見ていきます。
プロパティは、valueChange、valueConverter、equityComparerなどの追加のコールバックを備えた "Object.defineProperty"の単純なラッパーです。 プロパティを定義するとき、所有タイプとプロパティのタイプを指定します。
export const textProperty = new Property<MyButtonBase, string>({
name: "text",
defaultValue: "",
affectsLayout: true
});
textProperty.register(MyButtonBase);
textProperty
を見る:Property <MyButtonBase、string>
- 所有タイプはMyButtonBaseです。
つまり、このプロパティはMyButtonBaseのインスタンスで定義されます。
プロパティのタイプはstringなので、任意のテキストを受け入れます。
valueChangeイベントは、プロパティの値が変更されたときに実行されます。 プロパティのタイプがstringでない場合、valueConverterとequalityComparerを指定する必要があります。 このプロパティに(XMLやCSSなどから)文字列値が設定されている場合、valueConverterが呼び出され、可能であればその文字列を意味のある値に変換するか、できない場合は例外をスローする必要があります。 equalityComparerが指定されている場合、値がプロパティに設定されるたびに呼び出されます。 そこで、現在の値と新しい値が等しいかどうかを比較できます。 たとえば、プロパティのタイプがColorの場合、Color.equalsをequalityComparer関数として使用できるため、 Colorの新しいインスタンスが設定されている場合、現在の色と新しい色のargbが同じ値でも、比較子はfalseを返します。
Propertyコンストラクターにはもう1つのオプションがあります:effectsLayout: booleanです。 このプロパティに新しい値としてtrueを設定すると、新しいレイアウトパスがトリガーされます。 これはパフォーマンスの最適化として行われます。 Androidにはレイアウトシステムが統合されているため、ほとんどの場合、必要に応じて無効になります。 したがって、 'isIOS' ブーリアンプロパティを使用するなど、iOSのみに対してaffectsLayoutをtrueとして定義することにより、1つのネイティブ呼び出しをスキップします。 このプロパティがレイアウトに影響する可能性があることがわかっている場合、iOSには統合レイアウトシステムがないため、Propertyコンストラクタで指定する必要があります。
このプロパティを設定して要素のサイズや位置を変更する場合、フラグaffectsLayoutはtrue(主にiOSの場合)でなければなりません。 たとえば、この例では、ボタンのテキストを別の値に設定すると、ボタンの幅が広くなったり短くなったりして要素の寸法に影響するため、affectsLayout: isIOSとして指定します。 このプロパティが要素の位置/サイズを変更しない場合、affectsLayoutを指定する必要はありません。 たとえば、background-colorプロパティは要素の位置/サイズを変更しません。
CssPropertyは、Propertyタイプに非常に良く似ていますが、2つの小さな違いがあります。
export const myOpacityProperty = new CssProperty<Style, number>({
name: "myOpacity",
cssName: "my-opacity",
defaultValue: 1,
valueConverter: (v) => {
const x = parseFloat(v);
if (x < 0 || x > 1) {
throw new Error(`opacity accepts values in the range [0, 1]. Value: ${v}`);
}
return x;
}
});
myOpacityProperty.register(Style);
InheritedCssPropertyは、スタイルタイプで定義されたプロパティです。 これらは、CSSで設定でき、その子に値を伝播する継承可能なCSSプロパティです。 これらは、FontSize、FontWeight、Colorなどのプロパティです。
export const selectedBackgroundColorProperty = new InheritedCssProperty<Style, Color>({
name: "selectedBackgroundColor",
cssName: "selected-background-color",
equalityComparer: Color.equals,
valueConverter: (v) => new Color(v)
});
selectedBackgroundColorProperty.register(Style);
ShorthandPropertyは、CSSプロパティのショートハンド構文規則を提供します。たとえば、以下のような4つのすべてのマージンに対する明示的な構文の代わりに、
margin-top: 0;
margin-right: 10;
margin-bottom: 0;
margin-left: 10;
ユーザーは次のような簡略margin構文を使用したいでしょう。
margin: 0 10 0 10;
簡略marginプロパティを作成するには、すべてのCSSプロパティを定義する必要があります。 このようにこれらを使用して、短縮形のプロパティゲッターで構文ルールを設定できます。
const marginProperty = new ShorthandProperty<Style, string | PercentLength>({
name: "margin",
cssName: "margin",
getter: function (this: Style) {
if (PercentLength.equals(this.marginTop, this.marginRight) &&
PercentLength.equals(this.marginTop, this.marginBottom) &&
PercentLength.equals(this.marginTop, this.marginLeft)) {
return this.marginTop;
}
return `${PercentLength.convertToString(this.marginTop)} ${PercentLength.convertToString(this.marginRight)} ${PercentLength.convertToString(this.marginBottom)} ${PercentLength.convertToString(this.marginLeft)}`;
},
converter: convertToMargins
});
marginProperty.register(Style);
export const marginLeftProperty = new CssProperty<Style, PercentLength>({ name: "marginLeft", cssName: "margin-left", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginLeftProperty.register(Style);
export const marginRightProperty = new CssProperty<Style, PercentLength>({ name: "marginRight", cssName: "margin-right", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginRightProperty.register(Style);
export const marginTopProperty = new CssProperty<Style, PercentLength>({ name: "marginTop", cssName: "margin-top", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginTopProperty.register(Style);
export const marginBottomProperty = new CssProperty<Style, PercentLength>({ name: "marginBottom", cssName: "margin-bottom", defaultValue: zeroLength, affectsLayout: isIOS, equalityComparer: Length.equals, valueConverter: PercentLength.parse });
marginBottomProperty.register(Style);
CoerciblePropertyは、強制可能な機能を提供することにより、基本Propertyクラスを拡張するプロパティです。 プロパティを強制する必要がある場合の説明をわかりやすくするために、異なる数のアイテムを保持できるUI要素のselectedIndexプロパティで作業していると仮定しましょう。 基本ケースは、selectedIndexがアイテムの数内で異なることを示唆しますが、アイテムが動的に変更される場合(およびselectedIndexが長さの範囲内にない場合)は 何をカバーしますか? これは、値を強制できるプロパティで処理できる場合です。
selectedIndexアイテムの数に依存する強制可能なプロパティの作成
export const selectedIndexProperty = new CoercibleProperty<SegmentedBarBase, number>({
name: "selectedIndex", defaultValue: -1,
valueChanged: (target, oldValue, newValue) => {
target.notify(<SelectedIndexChangedEventData>{ eventName: SegmentedBarBase.selectedIndexChangedEvent, object: target, oldIndex: oldValue, newIndex: newValue });
},
// in this case the coerse value will change depending on whether the actual number of items
// is more or less than the value we want to appl for selectedIndex
coerceValue: (target, value) => {
let items = target.items;
if (items) {
let max = items.length - 1;
if (value < 0) {
value = 0;
}
if (value > max) {
value = max;
}
} else {
value = -1;
}
return value;
},
valueConverter: (v) => parseInt(v)
});
selectedIndexProperty.register(SegmentedBarBase);
itemsプロパティを設定するとき、selectedIndexを強制します
[itemsProperty.setNative](value: SegmentedBarItem[]) {
this.nativeViewProtected.clearAllTabs();
const newItems = value;
if (newItems) {
newItems.forEach((item, i, arr) => this.insertTab(item, i));
}
selectedIndexProperty.coerce(this);
}
プロパティを定義したら、次のようなタイプに登録する必要があります。
textProperty.register(MyButtonBase);
CssPropertiesは次のようにStyleクラスに登録する必要があります。
// Augmenting Style definition so it includes our myOpacity property
declare module "tns-core-modules/ui/styling/style" {
interface Style {
myOpacity: number;
}
}
// Defines 'myOpacity' property on Style class.
myOpacityProperty.register(Style);
この登録により、registerメソッドに渡される型のプロパティを定義します。
プロパティ値の変更時に通知を取得するには、addNameListenerメソッドのeventNameとしてpropertyNameChangeを指定する必要があります。
textField.addEventListener('textChange', handler...)
iosおよびandroidプロパティの代わりにnativeViewを使用します。 iosおよびandroidプロパティは互換性のために残されています。 ただし、すべてのビューライフサイクルメソッドとネイティブプロパティコールバック(以下で説明)は、nativeViewプロパティで動作する必要があります。
NativeScript 3.0はnativeViewリサイクルを導入しました。 nativeViewのリサイクルは、Androidで非常にコスト高な操作であるネイティブビューのインスタンス化を減らすことを目的としています。 リサイクルできるようにするには、ビューから公開されるすべてのプロパティがプロパティシステムである必要があります。
プロパティ値の最初の変更時に取得されるプロパティのデフォルト値を取得するメソッドがあります。 ビューがもう必要ないことがわかったら、ネイティブビューを元の状態にリセットし、同じタイプの将来のビューで再利用できるマップに配置します。 これには3つの新しい重要な方法があります。
createNativeView
- このメソッドをオーバーライドし、nativeViewを作成して返しますinitNativeView
- このメソッドでは、nativeViewにリスナー/ハンドラーを設定しますdisposeNativeView
- このメソッドでは、nativeViewとjavascriptオブジェクト間の参照をクリアしてメモリリークを回避し、後でネイティブビューを再利用する場合はネイティブビューを初期状態にリセットします。Androidでは、モジュールのルートにあるネイティブ型へのアクセスを避けます(ClickListenerは、実行時に呼び出される関数で宣言および実装されることに注意してください)。 これは、Androidランタイムが実行されていないホストマシンで生成されるV8スナップショット機能固有です。 重要なのは、モジュールのルート(たとえば、関数内ではない)でネイティブ型、メソッド、フィールド、名前空間などにアクセスする場合、コードはV8スナップショット機能と互換性がないことです。 最も簡単な回避策は、上記のinitializeClickListener関数のような関数でラップすることです。
この実装では、シングルトンリスナー(Androidの場合clickListener)およびハンドラー(iOSの場合handler)を使用して、 ネイティブクラスをインスタンス化する必要性を減らし、メモリ使用量を削減します。 可能であれば、そのような手法を使用してネイティブ呼び出しを減らすことをお勧めします。
ビュー階層をトラバースできるようにする2つのメソッドがあります。 どちらも、子に対して呼び出されるcallback関数を受け入れます。 コールバックはブール値を返す必要があり、偽である場合、反復が中断します。 これは、特定のビューを検索していて、見つかったらすぐに反復を停止する場合に特に便利です。
Viewの子を使用するには:
public eachChildView(callback: (child: View) => boolean): void
このメソッドは、以前は_eachChildView()と呼ばれていました。 Viewの子孫のみを返します。 たとえば、TabViewItemはViewBase型であるため、TabViewは各TabViewItemのビューを返します。
ViewBaseの子を使用するには:
public eachChild(callback: (child: ViewBase) => boolean): void;
このメソッドは、ViewBaseを含むすべてのビューを返します。 プロパティシステムによって使用され、ネイティブセッターの適用、継承されたプロパティの伝播、スタイルの適用などが行われます。 TabViewの場合、このメソッドは、TabViewItemsも返すので、CSSを使用してスタイル設定できます。
各UI要素は、Viewクラス(StackLayoutやLabelなど)を拡張し、 UI開発を容易にするために作成された一連のメソッドを備えています。 測定と配置に関連するメソッドは、すべてのレイアウト測定がパスしたことを確認するために、現在のPageのnavigatedToイベントで呼び出す必要があります。
<Page navigatedTo="onNavigatedTo">
<StackLayout id="myStack">
<Label text="Tap the button" />
<Button text="TAP" tap="" />
<Label id="myLabel" text="" />
</StackLayout>
</Page>
export function onNavigatedTo(args: EventData) {
const page = <Page>args.object;
let stack = <StackLayout>page.getViewById("myStack"); // e.g. StackLayout<myStack>@file:///app/page.xml:2:5;
let label = <Label>stack.getViewById("myLabel"); // e.g. Label<myLabel>@file:///app/main-page.xml:5:9;
}
function onNavigatedTo(args) {
const page = args.object;
let stack = page.getViewById("myStack"); // e.g. StackLayout<myStack>@file:///app/page.xml:2:5;
let label = stack.getViewById("myLabel"); // e.g. Label<myLabel>@file:///app/main-page.xml:5:9;
}
exports.onNavigatedTo = onNavigatedTo;
<StackLayout #myNgStack id="myStackId">
<Label text="Using ViewChild agains getViewById" />
</StackLayout>
import { ViewChild, ElementRef } from "@angular/core";
export class MyComponent {
@ViewChild("myNgStack") stackRef: ElementRef;
myNativeStack: StackLayout;
constructor(private _page: Page) { }
ngAfterViewInit() {
this._page.getViewById("myStackId");
this.myNativeStack = this.stackRef.nativeElement;
// this._page.getViewById("myStack") === this.myNativeStack
}
}
let stackSize: Size = stack.getActualSize();
let stackWidth = stackSize.width; // e.g. 411.42857142857144 DIP
let stackHeight = stackSize.height; // e.g. 603.4285714285714 DIP
let stackSize = stack.getActualSize();
let stackWidth = stackSize.width;
let stackHeight = stackSize.height;
let locationInWindow: Point = stack.getLocationInWindow();
let locationWindowX = locationInWindow.x; // e.g. 10
let locationWindowY = locationInWindow.y; // e.g. 80
let locationInWindow = stack.getLocationInWindow();
let locationWindowX = locationInWindow.x;
let locationWindowY = locationInWindow.y;
let locationOnScreen : Point = stack.getLocationOnScreen();
let locScreenX = locationOnScreen.x; // e.g. 10
let locScreenY = locationOnScreen.y; // e.g. 67.42857142857143
var locationOnScreen = stack.getLocationOnScreen();
var locScreenX = locationOnScreen.x;
var locScreenY = locationOnScreen.y;
let labelLocationRelativeToStack: Point = label.getLocationRelativeTo(stack);
let labelRelativeX = labelLocationRelativeToStack.x;
let labelRelativeY = labelLocationRelativeToStack.y;
let labelLocationRelativeToStack = label.getLocationRelativeTo(stack);
let labelRelativeX = labelLocationRelativeToStack.x;
let labelRelativeY = labelLocationRelativeToStack.y;