diff --git a/apps/app/ui-tests-app/css/elevation.css b/apps/app/ui-tests-app/css/elevation.css
new file mode 100644
index 000000000..5ebb8a2ab
--- /dev/null
+++ b/apps/app/ui-tests-app/css/elevation.css
@@ -0,0 +1,43 @@
+Label, Button {
+ text-align: center;
+ padding: 10;
+ margin: 16;
+ background-color: #bbb;
+}
+
+.elevation-0 {
+ android-elevation: 0;
+}
+
+.elevation-2 {
+ android-elevation: 2;
+}
+
+.elevation-4 {
+ android-elevation: 4;
+}
+
+.elevation-4:highlighted {
+ android-elevation: 2;
+}
+
+.elevation-8 {
+ android-elevation: 8;
+}
+
+.elevation-10 {
+ android-elevation: 10;
+}
+
+.pressed-z-10 {
+ android-dynamic-elevation-offset: 10;
+}
+
+.round {
+ color: #fff;
+ background-color: #ff1744;
+ border-radius: 50%; /* TODO kills elevation */
+ width: 80;
+ height: 80;
+ android-elevation: 8;
+}
\ No newline at end of file
diff --git a/apps/app/ui-tests-app/css/elevation.ts b/apps/app/ui-tests-app/css/elevation.ts
new file mode 100644
index 000000000..dd7e54701
--- /dev/null
+++ b/apps/app/ui-tests-app/css/elevation.ts
@@ -0,0 +1,20 @@
+import { Button } from "tns-core-modules/ui/button/button";
+import { EventData } from "tns-core-modules/ui/page/page";
+
+const states = [
+ { class: "", text: "default elevation" },
+ { class: "elevation-10", text: "elevetion 10" },
+ { class: "elevation-10 pressed-z-10", text: "elevetion 10 pressed-z 10" },
+ { class: "elevation-0", text: "elevetion 0" },
+]
+let currentState = 0;
+
+export function buttonTap(args: EventData) {
+ let btn: Button = args.object as Button;
+ currentState++;
+ if (currentState >= states.length) {
+ currentState = 0;
+ }
+ btn.className = states[currentState].class;
+ btn.text = states[currentState].text;
+}
\ No newline at end of file
diff --git a/apps/app/ui-tests-app/css/elevation.xml b/apps/app/ui-tests-app/css/elevation.xml
new file mode 100644
index 000000000..37a492e92
--- /dev/null
+++ b/apps/app/ui-tests-app/css/elevation.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/app/ui-tests-app/css/main-page.ts b/apps/app/ui-tests-app/css/main-page.ts
index 464da8189..c892c71ea 100644
--- a/apps/app/ui-tests-app/css/main-page.ts
+++ b/apps/app/ui-tests-app/css/main-page.ts
@@ -38,6 +38,7 @@ export function loadExamples() {
examples.set("margins-paddings-with-percentage", "css/margins-paddings-with-percentage");
examples.set("padding-and-border", "css/padding-and-border");
examples.set("combinators", "css/combinators");
+ examples.set("elevation", "css/elevation");
examples.set("styled-formatted-text", "css/styled-formatted-text");
examples.set("non-uniform-radius", "css/non-uniform-radius");
examples.set("missing-background-image", "css/missing-background-image");
diff --git a/tns-core-modules/ui/button/button.android.ts b/tns-core-modules/ui/button/button.android.ts
index 66db08e2d..2a2104652 100644
--- a/tns-core-modules/ui/button/button.android.ts
+++ b/tns-core-modules/ui/button/button.android.ts
@@ -3,11 +3,16 @@
paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty,
Length, zIndexProperty, textAlignmentProperty, TextAlignment
} from "./button-common";
+import { androidElevationProperty, androidDynamicElevationOffsetProperty } from "../styling/style-properties";
import { profile } from "../../profiling";
import { TouchGestureEventData, GestureTypes, TouchAction } from "../gestures";
+import { device } from "../../platform";
+import lazy from "../../utils/lazy";
export * from "./button-common";
+const sdkVersion = lazy(() => parseInt(device.sdkVersion));
+
interface ClickListener {
new(owner: Button): android.view.View.OnClickListener;
}
@@ -146,9 +151,28 @@ export class Button extends ButtonBase {
org.nativescript.widgets.ViewHelper.setZIndex(this.nativeViewProtected, value);
}
+ [androidElevationProperty.getDefault](): number {
+ if (sdkVersion() < 21) {
+ return 0;
+ }
+
+ // NOTE: Button widget has StateListAnimator that defines the elevation value and
+ // at the time of the getDefault() query the animator is not applied yet so we
+ // return the hardcoded @dimen/button_elevation_material value 2dp here instead
+ return 2;
+ }
+
+ [androidDynamicElevationOffsetProperty.getDefault](): number {
+ if (sdkVersion() < 21) {
+ return 0;
+ }
+
+ return 4; // 4dp @dimen/button_pressed_z_material
+ }
+
[textAlignmentProperty.setNative](value: TextAlignment) {
// Button initial value is center.
const newValue = value === "initial" ? "center" : value;
super[textAlignmentProperty.setNative](newValue);
}
-}
\ No newline at end of file
+}
diff --git a/tns-core-modules/ui/core/view-base/view-base.ts b/tns-core-modules/ui/core/view-base/view-base.ts
index 1863ce05d..caf868b0c 100644
--- a/tns-core-modules/ui/core/view-base/view-base.ts
+++ b/tns-core-modules/ui/core/view-base/view-base.ts
@@ -1018,7 +1018,7 @@ export const classNameProperty = new Property({
valueChanged(view: ViewBase, oldValue: string, newValue: string) {
let classes = view.cssClasses;
classes.clear();
- if (typeof newValue === "string") {
+ if (typeof newValue === "string" && newValue !== "") {
newValue.split(" ").forEach(c => classes.add(c));
}
view._onCssStateChange();
diff --git a/tns-core-modules/ui/core/view/view-common.ts b/tns-core-modules/ui/core/view/view-common.ts
index 26acd3e68..166f2205e 100644
--- a/tns-core-modules/ui/core/view/view-common.ts
+++ b/tns-core-modules/ui/core/view/view-common.ts
@@ -683,6 +683,20 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
this.style.scaleY = value;
}
+ get androidElevation(): number {
+ return this.style.androidElevation;
+ }
+ set androidElevation(value: number) {
+ this.style.androidElevation = value;
+ }
+
+ get androidDynamicElevationOffset(): number {
+ return this.style.androidDynamicElevationOffset;
+ }
+ set androidDynamicElevationOffset(value: number) {
+ this.style.androidDynamicElevationOffset = value;
+ }
+
//END Style property shortcuts
public automationText: string;
diff --git a/tns-core-modules/ui/core/view/view.android.ts b/tns-core-modules/ui/core/view/view.android.ts
index 7e27b8e5a..7c4e92e60 100644
--- a/tns-core-modules/ui/core/view/view.android.ts
+++ b/tns-core-modules/ui/core/view/view.android.ts
@@ -15,19 +15,27 @@ import {
minWidthProperty, minHeightProperty, widthProperty, heightProperty,
marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty,
rotateProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty,
- zIndexProperty, backgroundInternalProperty
+ zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty
} from "../../styling/style-properties";
import { Background, ad as androidBackground } from "../../styling/background";
import { profile } from "../../../profiling";
import { topmost } from "../../frame/frame-stack";
import { AndroidActivityBackPressedEventData, android as androidApp } from "../../../application";
+import { device } from "../../../platform";
+import lazy from "../../../utils/lazy";
export * from "./view-common";
const DOMID = "_domId";
const androidBackPressedEvent = "androidBackPressed";
+const shortAnimTime = 17694720; // android.R.integer.config_shortAnimTime
+const statePressed = 16842919; // android.R.attr.state_pressed
+const stateEnabled = 16842910; // android.R.attr.state_enabled
+
+const sdkVersion = lazy(() => parseInt(device.sdkVersion));
+
const modalMap = new Map();
let TouchListener: TouchListener;
@@ -255,6 +263,8 @@ export class View extends ViewCommon {
private layoutChangeListener: android.view.View.OnLayoutChangeListener;
private _manager: android.support.v4.app.FragmentManager;
private _rootManager: android.support.v4.app.FragmentManager;
+ private _originalElevation: number;
+ private _originalStateListAnimator: any; /* android.animation.StateListAnimator; */
nativeViewProtected: android.view.View;
@@ -709,6 +719,74 @@ export class View extends ViewCommon {
this.nativeViewProtected.setAlpha(float(value));
}
+ [androidElevationProperty.getDefault](): number {
+ if (sdkVersion() < 21) {
+ return 0;
+ }
+
+ // NOTE: overriden in Button implementation as for widgets with StateListAnimator (Button)
+ // nativeView.getElevation() returns 0 at the time of the getDefault() query
+ return layout.toDeviceIndependentPixels((this.nativeViewProtected).getElevation());
+ }
+ [androidElevationProperty.setNative](value: number) {
+ if (sdkVersion() < 21) {
+ return;
+ }
+
+ this.refreshStateListAnimator();
+ }
+
+ [androidDynamicElevationOffsetProperty.getDefault](): number {
+ return 0;
+ }
+ [androidDynamicElevationOffsetProperty.setNative](value: number) {
+ if (sdkVersion() < 21) {
+ return;
+ }
+
+ this.refreshStateListAnimator();
+ }
+
+ private refreshStateListAnimator() {
+ const nativeView: any = this.nativeViewProtected;
+
+ const ObjectAnimator = android.animation.ObjectAnimator;
+ const AnimatorSet = android.animation.AnimatorSet;
+
+ const duration = nativeView.getContext().getResources().getInteger(shortAnimTime) / 2;
+ const elevation = layout.toDevicePixels(this.androidElevation || 0);
+ const z = layout.toDevicePixels(0);
+ const pressedZ = layout.toDevicePixels(this.androidDynamicElevationOffset || 0);
+
+ const pressedSet = new AnimatorSet();
+ pressedSet.playTogether(java.util.Arrays.asList([
+ ObjectAnimator.ofFloat(nativeView, "translationZ", [pressedZ])
+ .setDuration(duration),
+ ObjectAnimator.ofFloat(nativeView, "elevation", [elevation])
+ .setDuration(0),
+ ]));
+
+ const notPressedSet = new AnimatorSet();
+ notPressedSet.playTogether(java.util.Arrays.asList([
+ ObjectAnimator.ofFloat(nativeView, "translationZ", [z])
+ .setDuration(duration),
+ ObjectAnimator.ofFloat(nativeView, "elevation", [elevation])
+ .setDuration(0),
+ ]));
+
+ const defaultSet = new AnimatorSet();
+ defaultSet.playTogether(java.util.Arrays.asList([
+ ObjectAnimator.ofFloat(nativeView, "translationZ", [0]).setDuration(0),
+ ObjectAnimator.ofFloat(nativeView, "elevation", [0]).setDuration(0),
+ ]));
+
+ const stateListAnimator = new (android.animation).StateListAnimator();
+ stateListAnimator.addState([statePressed, stateEnabled], pressedSet);
+ stateListAnimator.addState([stateEnabled], notPressedSet);
+ stateListAnimator.addState([], defaultSet);
+ nativeView.setStateListAnimator(stateListAnimator);
+ }
+
[horizontalAlignmentProperty.getDefault](): HorizontalAlignment {
return org.nativescript.widgets.ViewHelper.getHorizontalAlignment(this.nativeViewProtected);
}
@@ -946,7 +1024,7 @@ function createNativePercentLengthProperty(options: NativePercentLengthPropertyO
const { getter, setter, auto = 0 } = options;
let setPixels, getPixels, setPercent;
if (getter) {
- View.prototype[getter] = function (this: View): PercentLength {
+ View.prototype[getter] = function(this: View): PercentLength {
if (options) {
setPixels = options.setPixels;
getPixels = options.getPixels;
@@ -962,7 +1040,7 @@ function createNativePercentLengthProperty(options: NativePercentLengthPropertyO
}
}
if (setter) {
- View.prototype[setter] = function (this: View, length: PercentLength) {
+ View.prototype[setter] = function(this: View, length: PercentLength) {
if (options) {
setPixels = options.setPixels;
getPixels = options.getPixels;
diff --git a/tns-core-modules/ui/core/view/view.d.ts b/tns-core-modules/ui/core/view/view.d.ts
index 9b9fb84d9..5325ab9ed 100644
--- a/tns-core-modules/ui/core/view/view.d.ts
+++ b/tns-core-modules/ui/core/view/view.d.ts
@@ -224,6 +224,16 @@ export abstract class View extends ViewBase {
*/
color: Color;
+ /**
+ * Gets or sets the elevation of the android view.
+ */
+ androidElevation: number;
+
+ /**
+ * Gets or sets the dynamic elevation offset of the android view.
+ */
+ androidDynamicElevationOffset: number;
+
/**
* Gets or sets the background style property.
*/
diff --git a/tns-core-modules/ui/styling/style-properties.ts b/tns-core-modules/ui/styling/style-properties.ts
index 185446827..0963cf353 100644
--- a/tns-core-modules/ui/styling/style-properties.ts
+++ b/tns-core-modules/ui/styling/style-properties.ts
@@ -1099,3 +1099,9 @@ export const visibilityProperty = new CssProperty