Initial prototype of Frame + Page + Navigation.

This commit is contained in:
atanasovg
2014-06-12 17:37:55 +03:00
parent ef7c3ce677
commit 2c4781db01
26 changed files with 520 additions and 80 deletions

View File

@ -153,14 +153,18 @@
<DependentUpon>text.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\core\bindable.ts" />
<TypeScriptCompile Include="ui\core\index.ts" />
<TypeScriptCompile Include="ui\core\proxy.ts" />
<TypeScriptCompile Include="ui\core\view.android.ts">
<DependentUpon>view.d.ts</DependentUpon>
<TypeScriptCompile Include="ui\frame\frame-common.ts">
<DependentUpon>frame.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\core\view.ios.ts">
<DependentUpon>view.d.ts</DependentUpon>
<TypeScriptCompile Include="ui\frame\frame.android.ts">
<DependentUpon>frame.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\frame\frame.d.ts" />
<TypeScriptCompile Include="ui\frame\frame.ios.ts">
<DependentUpon>frame.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\frame\index.ts" />
<TypeScriptCompile Include="ui\image\image.android.ts">
<DependentUpon>image.d.ts</DependentUpon>
</TypeScriptCompile>
@ -177,14 +181,22 @@
<TypeScriptCompile Include="ui\label\label.ios.ts">
<DependentUpon>label.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-input\index.ts" />
<TypeScriptCompile Include="ui\text-input\text-input.android.ts">
<DependentUpon>text-input.d.ts</DependentUpon>
<TypeScriptCompile Include="ui\pages\index.ts" />
<TypeScriptCompile Include="ui\pages\page-common.ts">
<DependentUpon>page.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-input\text-input.ios.ts">
<DependentUpon>text-input.d.ts</DependentUpon>
<TypeScriptCompile Include="ui\pages\page.android.ts">
<DependentUpon>page.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\pages\page.d.ts" />
<TypeScriptCompile Include="ui\text-field\index.ts" />
<TypeScriptCompile Include="ui\text-field\text-field.android.ts">
<DependentUpon>text-field.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-field\text-field.d.ts" />
<TypeScriptCompile Include="ui\text-field\text-field.ios.ts">
<DependentUpon>text-field.d.ts</DependentUpon>
</TypeScriptCompile>
<TypeScriptCompile Include="ui\text-input\text-input.d.ts" />
<TypeScriptCompile Include="utils\module-merge.ts" />
<TypeScriptCompile Include="utils\utils_android.ts" />
<TypeScriptCompile Include="utils\utils_ios.ts" />
@ -247,7 +259,7 @@
<TypeScriptCompile Include="libjs.d.ts" />
</ItemGroup>
<ItemGroup>
<TypeScriptCompile Include="ui\core\view.d.ts" />
<TypeScriptCompile Include="ui\core\view.ts" />
<TypeScriptCompile Include="ui\core\observable.ts" />
<TypeScriptCompile Include="ui\dialogs\dialogs.android.ts">
<DependentUpon>dialogs.d.ts</DependentUpon>

View File

@ -1,5 +1,6 @@
import appModule = require("application/application-common");
import dts = require("application");
import frame = require("ui/frame");
// merge the exports of the application_common file with the exports of this file
declare var exports;
@ -112,7 +113,7 @@ class AndroidApplication implements dts.AndroidApplication {
public foregroundActivity: android.app.Activity;
public startActivity: android.app.Activity;
public packageName: string;
public getActivity: (intent: android.content.Intent) => any;
// public getActivity: (intent: android.content.Intent) => any;
public onActivityCreated: (activity: android.app.Activity, bundle: android.os.Bundle) => void;
public onActivityDestroyed: (activity: android.app.Activity) => void;
@ -128,7 +129,15 @@ class AndroidApplication implements dts.AndroidApplication {
this.nativeApp = nativeApp;
this.packageName = nativeApp.getPackageName();
this.context = nativeApp.getApplicationContext();
this.getActivity = undefined;
}
public getActivity(intent: android.content.Intent): any {
var currentPage = rootFrame.currentPage;
if (!currentPage) {
throw new Error("Root frame not navigated to a page.");
}
return currentPage.android.getActivityExtends();
}
public init() {
@ -136,4 +145,7 @@ class AndroidApplication implements dts.AndroidApplication {
this.nativeApp.registerActivityLifecycleCallbacks(this._eventsToken);
this.context = this.nativeApp.getApplicationContext();
}
}
}
// The root frame of the application
export var rootFrame = new frame.Frame();

View File

@ -1,11 +1,13 @@

declare module "application" {
import frame = require("ui/frame");
export var rootFrame: frame.Frame;
/**
* The main entry point event. This method is expected to return an instance of the root UI for the application.
* This will be an Activity extends for Android and a RootViewController for iOS.
* The main entry point event. This method is expected to use the root frame to navigate to the main application page.
*/
export function onLaunch(): any;
export function onLaunch(): void;
/**
* This method will be called when the Application is suspended.
@ -85,7 +87,7 @@ declare module "application" {
* This method is called by the JavaScript Bridge when navigation to a new activity is triggered.
* The return value of this method should be com.tns.NativeScriptActivity.extends implementation.
*/
getActivity: (intent: android.content.Intent) => any;
getActivity(intent: android.content.Intent): any;
/**
* Direct handler of the android.app.Application.ActivityLifecycleCallbacks.onActivityCreated method.

View File

@ -338,6 +338,7 @@ declare module android {
}
*/
declare var app;
declare var telerik;
declare var gc: () => any;

View File

@ -37,6 +37,11 @@ export class Bindable extends observable.Observable {
}
}
public setPropertyCore(data: observable.PropertyChangeData) {
super.setPropertyCore(data);
this.updateTwoWayBinding(data.propertyName, data.value);
}
public getBinding(propertyName: string) {
return this._bindings[propertyName];
}

View File

@ -1 +0,0 @@


View File

@ -31,6 +31,13 @@ export class Observable {
private _trackChanging = false;
constructor(body?: any) {
if (body) {
for (var key in body) {
// TODO: Is this correct
this[key] = body[key];
}
}
this.on = this.addEventListener = this.addObserver;
this.off = this.removeEventListener = this.removeObserver;
}

View File

@ -4,6 +4,7 @@ import bindable = require("ui/core/bindable");
export class ProxyObject extends bindable.Bindable {
public setPropertyCore(data: observable.PropertyChangeData) {
this.setNativeProperty(data);
this.updateTwoWayBinding(data.propertyName, data.value);
}
public setNativeProperty(data: observable.PropertyChangeData) {

View File

@ -1,11 +0,0 @@
import proxy = require("ui/core/proxy");
export class View extends proxy.ProxyObject {
public addToParent(parent: android.view.ViewGroup) {
var nativeInstance: android.view.View = this["android"];
if (nativeInstance) {
// TODO: Check for existing parent
parent.addView(nativeInstance);
}
}
}

7
ui/core/view.d.ts vendored
View File

@ -1,7 +0,0 @@
import proxy = require("ui/core/proxy");
export declare class View extends proxy.ProxyObject {
public addToParent(parent: any);
android: any;
ios: any;
}

View File

@ -1,11 +0,0 @@
import proxy = require("ui/core/proxy");
export class View extends proxy.ProxyObject {
public addToParent(parent: UIKit.UIView) {
var nativeInstance: UIKit.UIView = this["ios"];
if (nativeInstance) {
// TODO: Check for existing parent
parent.addSubview(nativeInstance);
}
}
}

99
ui/core/view.ts Normal file
View File

@ -0,0 +1,99 @@
import proxy = require("ui/core/proxy");
import application = require("application");
export class View extends proxy.ProxyObject {
private _parent: Panel;
public onInitialized(content: android.content.Context) {
// TODO: This is used by Android, rethink this routine
}
public addToParent(native: any) {
// TODO: Temporary
if (application.ios && this.ios) {
native.addSubview(this.ios);
} else if (application.android && this.android) {
native.addView(this.android);
}
}
/**
* Gets the Panel instance that parents this view. This property is read-only.
*/
get parent(): Panel {
return this._parent;
}
/**
* Gets the android-specific native instance that lies behind this view. Will be available if running on an Android platform.
*/
get android(): any {
return undefined;
}
/**
* Gets the ios-specific native instance that lies behind this view. Will be available if running on an Android platform.
*/
get ios(): any {
return undefined;
}
// TODO: Should these be public?
public onAddedToParent(parent: Panel) {
this._parent = parent;
// TODO: Attach to parent - e.g. update data context, bindings, styling, etc.
}
public onRemovedFromParent() {
this._parent = null;
// TODO: Detach from parent.
}
}
/**
* The Panel class represents an extended View which can have other views as children.
*/
export class Panel extends View {
private _children: Array<View>;
constructor() {
super();
this._children = new Array<View>();
}
public addChild(child: View) {
// Validate child is not parented
if (child.parent) {
var message;
if (child.parent === this) {
message = "View already added to this panel.";
} else {
message = "View is already a child of another panel.";
}
throw new Error(message);
}
this._children.push(child);
child.onAddedToParent(this);
}
public removeChild(child: View) {
if (!child) {
return;
}
if (child.parent !== this) {
throw new Error("View is not parented by this panel.");
}
var index = this._children.indexOf(child);
if (index < 0) {
throw new Error("View not found in children collection.");
}
this._children.splice(index, 1);
child.onRemovedFromParent();
}
}

114
ui/frame/frame-common.ts Normal file
View File

@ -0,0 +1,114 @@
import frame = require("ui/frame");
import view = require("ui/core/view");
import pages = require("ui/pages");
enum NavigationType {
New,
Back,
Forward
}
export class Frame extends view.View implements frame.Frame {
private _backStack: Array<frame.PageNavigationEntry>;
private _forwardStack: Array<frame.PageNavigationEntry>;
private _currentEntry: frame.PageNavigationEntry;
private _currentPage: pages.Page;
private _navigationType: NavigationType;
// TODO: Currently our navigation will not be synchronized in case users directly call native navigation methods like Activity.startActivity.
constructor() {
super();
this._backStack = new Array<frame.PageNavigationEntry>();
this._forwardStack = new Array<frame.PageNavigationEntry>();
this._navigationType = NavigationType.New;
}
public canGoBack(): boolean {
return this._backStack.length > 0;
}
public canGoForward(): boolean {
return this._forwardStack.length > 0;
}
public goBack() {
if (!this.canGoBack()) {
// TODO: Do we need to throw an error?
return;
}
var entry = this._backStack.pop();
this._navigationType = NavigationType.Back;
this.navigate(entry);
}
public goForward() {
if (!this.canGoForward()) {
// TODO: Do we need to throw an error?
return;
}
var entry = this._forwardStack.pop();
this._navigationType = NavigationType.Forward;
this.navigate(entry);
}
public navigate(entry: frame.PageNavigationEntry) {
if (this._currentPage) {
this._backStack.push(this._currentEntry);
}
// perform the actual navigation, depending on the requested navigation type
switch (this._navigationType) {
case NavigationType.New:
this.navigateCore(entry.context);
break;
case NavigationType.Back:
this.goBackCore();
if (this._currentPage) {
this._forwardStack.push(this._currentEntry);
}
break;
case NavigationType.Forward:
this.goForwardCore();
if (this._currentPage) {
this._backStack.push(this._currentEntry);
}
break;
}
// TODO: We assume here that there is a Page object in the exports of the required module. This should be well documented.
this._currentPage = require(entry.pageModuleName).Page;
this._currentPage.frame = this;
this._currentEntry = entry;
// notify the page
this._currentPage.onNavigatedTo(entry.context);
// reset the navigation type back to new
this._navigationType = NavigationType.New;
}
public goBackCore() {
}
public goForwardCore() {
}
public navigateCore(context: any) {
}
get backStack(): Array<frame.PageNavigationEntry> {
return this._backStack;
}
get forwardStack(): Array<frame.PageNavigationEntry> {
return this._forwardStack;
}
get currentPage(): pages.Page {
return this._currentPage;
}
}

35
ui/frame/frame.android.ts Normal file
View File

@ -0,0 +1,35 @@
import frameCommon = require("ui/frame/frame-common");
import frame = require("ui/frame");
import pages = require("ui/pages");
import application = require("application");
export class Frame extends frameCommon.Frame {
public navigateCore(context: any) {
if (this.backStack.length === 0) {
// When navigating for the very first time we do not want to start an activity
// TODO: Revisit/polish this behavior
return;
}
var activity = this.currentPage.android.activity;
if (!activity) {
throw new Error("Current page does have an activity created.");
}
var intent = new android.content.Intent(activity, (<any>com).tns.NativeScriptActivity.class);
activity.startActivity(intent);
}
public goBackCore() {
var activity = this.currentPage.android.activity;
if (!activity) {
throw new Error("Current page does have an activity created.");
}
// TODO: This is not true in all cases, update once added support for parent activity in Android manifest
activity.finish();
}
public goForwardCore() {
}
}

24
ui/frame/frame.d.ts vendored Normal file
View File

@ -0,0 +1,24 @@
declare module "ui/frame" {
import view = require("ui/core/view");
// There is a cyclic reference here (pages module requires frame) but it is intented and needed.
import pages = require("ui/pages");
export class Frame extends view.View {
goBack();
canGoBack(): boolean;
goForward();
canGoForward(): boolean;
navigate(entry: PageNavigationEntry);
currentPage: pages.Page;
backStack: Array<PageNavigationEntry>;
forwardStack: Array<PageNavigationEntry>;
}
export interface PageNavigationEntry {
pageModuleName: string;
context?: any;
}
}

2
ui/frame/index.ts Normal file
View File

@ -0,0 +1,2 @@
declare var module, require;
module.exports = require("ui/frame/frame");

View File

@ -2,36 +2,23 @@
import view = require("ui/core/view");
import application = require("application");
var TEXT = "text";
// this is the name of the property to store text locally until attached to a valid Context
var TEXTPRIVATE = "_text";
export class Label extends view.View {
private static textProperty = "text";
private _android: android.widget.TextView;
constructor() {
super();
}
// TODO: Verify that this is always true
var context = application.android.currentContext;
if (!context) {
// TODO: Delayed loading?
public onInitialized(context: android.content.Context) {
if (!this._android) {
// TODO: We need to decide whether we will support context switching and if yes - to implement it.
this.createUI(context);
}
this._android = new android.widget.TextView(context);
var that = this;
var textWatcher = new android.text.TextWatcher({
beforeTextChanged: function (text: string, start: number, count: number, after: number) {
},
onTextChanged: function (text: string, start: number, before: number, count: number) {
},
afterTextChanged: function (editable: android.text.IEditable) {
//if (that.hasObservers(observable.Observable.propertyChangeEvent)) {
// var data = that.createPropertyChangeData(TextView.textProperty, that.text);
// that.notify(data);
//}
that.updateTwoWayBinding("text", editable.toString());
}
});
this._android.addTextChangedListener(textWatcher);
}
get android(): android.widget.TextView {
@ -39,17 +26,45 @@ export class Label extends view.View {
}
get text(): string {
if (!this._android) {
return this[TEXTPRIVATE];
}
return this._android.getText().toString();
}
set text(value: string) {
this.setProperty(Label.textProperty, value);
this.setProperty(TEXT, value);
}
public setNativeProperty(data: observable.PropertyChangeData) {
// TODO: Will this be a gigantic if-else switch?
if (data.propertyName === Label.textProperty) {
this._android.setText(data.value);
if (data.propertyName === TEXT) {
if (this._android) {
this._android.setText(data.value);
} else {
this[TEXTPRIVATE] = data.value;
}
} else if (true) {
}
}
private createUI(context: android.content.Context) {
this._android = new android.widget.TextView(context);
if (this[TEXTPRIVATE]) {
this._android.setText(this[TEXTPRIVATE]);
delete this[TEXTPRIVATE];
}
// TODO: Do we need to listen for text change here?
//var that = this;
//var textWatcher = new android.text.TextWatcher({
// beforeTextChanged: function (text: string, start: number, count: number, after: number) {
// },
// onTextChanged: function (text: string, start: number, before: number, count: number) {
// },
// afterTextChanged: function (editable: android.text.IEditable) {
// that.updateTwoWayBinding("text", editable.toString());
// }
//});
//this._android.addTextChangedListener(textWatcher);
}
}

2
ui/pages/index.ts Normal file
View File

@ -0,0 +1,2 @@
declare var module, require;
module.exports = require("ui/pages/page");

36
ui/pages/page-common.ts Normal file
View File

@ -0,0 +1,36 @@
import view = require("ui/core/view");
import dts = require("ui/pages");
import frame = require("ui/frame");
import application = require("application");
export class Page extends view.View implements dts.Page {
private _contentView: view.View;
private _frame: frame.Frame;
private _navigationContext: any;
public onLoaded: () => any;
get contentView(): view.View {
return this._contentView;
}
set contentView(value: view.View) {
this._contentView = value;
// TODO: Check if page is already loaded and update as needed
}
get frame(): frame.Frame {
return this._frame;
}
set frame(value: frame.Frame) {
// TODO: This method is called internally, check how to hide the setter from users.
this._frame = value;
}
public onNavigatedTo(context: any) {
this._navigationContext = context;
}
public onNavigatedFrom() {
}
}

81
ui/pages/page.android.ts Normal file
View File

@ -0,0 +1,81 @@
import definition = require("ui/pages");
import pageCommon = require("ui/pages/page-common");
export class Page extends pageCommon.Page {
private _android: AndroidPage;
constructor() {
super();
this._android = new AndroidPage(this);
}
get android(): definition.AndroidPage {
return this._android;
}
}
class AndroidPage implements definition.AndroidPage {
private _ownerPage: definition.Page;
private _body: any;
private _activityExtends: any;
private _activity: android.app.Activity;
constructor(ownerPage: definition.Page) {
this._ownerPage = ownerPage;
}
get activity(): android.app.Activity {
return this._activity;
}
get activityBody(): any {
return this._body;
}
set activityBody(value: any) {
if (this._activityExtends) {
throw new Error("Activity already loaded and its body may not be changed.");
}
this._body = value;
}
public getActivityExtends(): any {
if (!this._body) {
this.rebuildBody();
}
return this._activityExtends;
}
public resetBody() {
this._body = null;
this._activityExtends = null;
}
private rebuildBody() {
var that = this;
this._body = {
onCreate: function () {
that._activity = this;
this.super.onCreate(null);
var view = that._ownerPage.contentView;
if (view) {
// TODO: Notify the entire visual tree for being initialized
view.onInitialized(that._activity);
that._activity.setContentView(view.android);
}
}
}
if (this._ownerPage.onLoaded) {
this._body.onStart = function () {
this.super.onStart();
that._ownerPage.onLoaded();
}
}
this._activityExtends = (<any>com).tns.NativeScriptActivity.extends(this._body);
}
}

25
ui/pages/page.d.ts vendored Normal file
View File

@ -0,0 +1,25 @@
declare module "ui/pages" {
import view = require("ui/core/view");
import frame = require("ui/frame");
export class Page extends view.View {
contentView: view.View;
android: AndroidPage;
/**
* Gets the Frame object controlling this instance.
*/
frame: frame.Frame;
onNavigatedTo(context: any): void;
onNavigatedFrom(): void;
onLoaded: () => void;
}
export interface AndroidPage {
activity: android.app.Activity;
activityBody: any;
getActivityExtends(): any;
}
}

View File

@ -20,11 +20,7 @@ export class TextInput extends view.View {
onTextChanged: function (text: string, start: number, before: number, count: number) {
},
afterTextChanged: function (editable: android.text.IEditable) {
//if (that.hasObservers(observable.Observable.propertyChangeEvent)) {
// var data = that.createPropertyChangeData(TextView.textProperty, that.text);
// that.notify(data);
//}
that.updateTwoWayBinding("text", editable.toString());
that.updateTwoWayBinding(TextInput.textProperty, editable.toString());
}
});
this._android.addTextChangedListener(textWatcher);

View File

@ -0,0 +1 @@