Skip to content

Commit cf533a7

Browse files
edusperonimanoldonev
authored andcommitted
feat(android): elevation shadow support (#7136)
1 parent f875491 commit cf533a7

12 files changed

Lines changed: 236 additions & 11 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
Label, Button {
2+
text-align: center;
3+
padding: 10;
4+
margin: 16;
5+
background-color: #bbb;
6+
}
7+
8+
.elevation-0 {
9+
android-elevation: 0;
10+
}
11+
12+
.elevation-2 {
13+
android-elevation: 2;
14+
}
15+
16+
.elevation-4 {
17+
android-elevation: 4;
18+
}
19+
20+
.elevation-4:highlighted {
21+
android-elevation: 2;
22+
}
23+
24+
.elevation-8 {
25+
android-elevation: 8;
26+
}
27+
28+
.elevation-10 {
29+
android-elevation: 10;
30+
}
31+
32+
.pressed-z-10 {
33+
android-dynamic-elevation-offset: 10;
34+
}
35+
36+
.round {
37+
color: #fff;
38+
background-color: #ff1744;
39+
border-radius: 50%; /* TODO kills elevation */
40+
width: 80;
41+
height: 80;
42+
android-elevation: 8;
43+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Button } from "tns-core-modules/ui/button/button";
2+
import { EventData } from "tns-core-modules/ui/page/page";
3+
4+
const states = [
5+
{ class: "", text: "default elevation" },
6+
{ class: "elevation-10", text: "elevetion 10" },
7+
{ class: "elevation-10 pressed-z-10", text: "elevetion 10 pressed-z 10" },
8+
{ class: "elevation-0", text: "elevetion 0" },
9+
]
10+
let currentState = 0;
11+
12+
export function buttonTap(args: EventData) {
13+
let btn: Button = args.object as Button;
14+
currentState++;
15+
if (currentState >= states.length) {
16+
currentState = 0;
17+
}
18+
btn.className = states[currentState].class;
19+
btn.text = states[currentState].text;
20+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="onLoaded">
2+
<ScrollView>
3+
<StackLayout backgroundColor="aliceblue" androidElevation="2" padding="10" margin="20">
4+
<Button text="default elevation" tap="buttonTap"/>
5+
6+
<Button class="elevation-0" text="elevation 0"/>
7+
8+
<Label class="elevation-2" text="Label with elevation 2"/>
9+
10+
<StackLayout androidElevation="4" backgroundColor="#ff1744" horizontalAlignment="center" margin="20">
11+
<Button class="elevation-2" text="elevation 2"/>
12+
<Button class="elevation-4" text="elevation 4"/>
13+
</StackLayout>
14+
15+
<StackLayout orientation="horizontal" horizontalAlignment="center">
16+
<Button class="round" androidElevation="4" text="el. 4"/>
17+
<Button class="elevation-8 round" text="el. 8"/>
18+
</StackLayout>
19+
20+
<Label class="elevation-8" text="elevation 8"/>
21+
<Button class="elevation-8" text="elevation 8, with radius" borderRadius="4"/>
22+
<Button class="elevation-10" text="elevation 10"/>
23+
</StackLayout>
24+
</ScrollView>
25+
</Page>

apps/app/ui-tests-app/css/main-page.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export function loadExamples() {
3838
examples.set("margins-paddings-with-percentage", "css/margins-paddings-with-percentage");
3939
examples.set("padding-and-border", "css/padding-and-border");
4040
examples.set("combinators", "css/combinators");
41+
examples.set("elevation", "css/elevation");
4142
examples.set("styled-formatted-text", "css/styled-formatted-text");
4243
examples.set("non-uniform-radius", "css/non-uniform-radius");
4344
examples.set("missing-background-image", "css/missing-background-image");

tns-core-modules/ui/button/button.android.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@
33
paddingLeftProperty, paddingTopProperty, paddingRightProperty, paddingBottomProperty,
44
Length, zIndexProperty, textAlignmentProperty, TextAlignment
55
} from "./button-common";
6+
import { androidElevationProperty, androidDynamicElevationOffsetProperty } from "../styling/style-properties";
67
import { profile } from "../../profiling";
78
import { TouchGestureEventData, GestureTypes, TouchAction } from "../gestures";
9+
import { device } from "../../platform";
10+
import lazy from "../../utils/lazy";
811

912
export * from "./button-common";
1013

14+
const sdkVersion = lazy(() => parseInt(device.sdkVersion));
15+
1116
interface ClickListener {
1217
new(owner: Button): android.view.View.OnClickListener;
1318
}
@@ -146,9 +151,28 @@ export class Button extends ButtonBase {
146151
org.nativescript.widgets.ViewHelper.setZIndex(this.nativeViewProtected, value);
147152
}
148153

154+
[androidElevationProperty.getDefault](): number {
155+
if (sdkVersion() < 21) {
156+
return 0;
157+
}
158+
159+
// NOTE: Button widget has StateListAnimator that defines the elevation value and
160+
// at the time of the getDefault() query the animator is not applied yet so we
161+
// return the hardcoded @dimen/button_elevation_material value 2dp here instead
162+
return 2;
163+
}
164+
165+
[androidDynamicElevationOffsetProperty.getDefault](): number {
166+
if (sdkVersion() < 21) {
167+
return 0;
168+
}
169+
170+
return 4; // 4dp @dimen/button_pressed_z_material
171+
}
172+
149173
[textAlignmentProperty.setNative](value: TextAlignment) {
150174
// Button initial value is center.
151175
const newValue = value === "initial" ? "center" : value;
152176
super[textAlignmentProperty.setNative](newValue);
153177
}
154-
}
178+
}

tns-core-modules/ui/core/view-base/view-base.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,7 @@ export const classNameProperty = new Property<ViewBase, string>({
10181018
valueChanged(view: ViewBase, oldValue: string, newValue: string) {
10191019
let classes = view.cssClasses;
10201020
classes.clear();
1021-
if (typeof newValue === "string") {
1021+
if (typeof newValue === "string" && newValue !== "") {
10221022
newValue.split(" ").forEach(c => classes.add(c));
10231023
}
10241024
view._onCssStateChange();

tns-core-modules/ui/core/view/view-common.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,20 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
683683
this.style.scaleY = value;
684684
}
685685

686+
get androidElevation(): number {
687+
return this.style.androidElevation;
688+
}
689+
set androidElevation(value: number) {
690+
this.style.androidElevation = value;
691+
}
692+
693+
get androidDynamicElevationOffset(): number {
694+
return this.style.androidDynamicElevationOffset;
695+
}
696+
set androidDynamicElevationOffset(value: number) {
697+
this.style.androidDynamicElevationOffset = value;
698+
}
699+
686700
//END Style property shortcuts
687701

688702
public automationText: string;

tns-core-modules/ui/core/view/view.android.ts

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,27 @@ import {
1515
minWidthProperty, minHeightProperty, widthProperty, heightProperty,
1616
marginLeftProperty, marginTopProperty, marginRightProperty, marginBottomProperty,
1717
rotateProperty, scaleXProperty, scaleYProperty, translateXProperty, translateYProperty,
18-
zIndexProperty, backgroundInternalProperty
18+
zIndexProperty, backgroundInternalProperty, androidElevationProperty, androidDynamicElevationOffsetProperty
1919
} from "../../styling/style-properties";
2020

2121
import { Background, ad as androidBackground } from "../../styling/background";
2222
import { profile } from "../../../profiling";
2323
import { topmost } from "../../frame/frame-stack";
2424
import { AndroidActivityBackPressedEventData, android as androidApp } from "../../../application";
25+
import { device } from "../../../platform";
26+
import lazy from "../../../utils/lazy";
2527

2628
export * from "./view-common";
2729

2830
const DOMID = "_domId";
2931
const androidBackPressedEvent = "androidBackPressed";
3032

33+
const shortAnimTime = 17694720; // android.R.integer.config_shortAnimTime
34+
const statePressed = 16842919; // android.R.attr.state_pressed
35+
const stateEnabled = 16842910; // android.R.attr.state_enabled
36+
37+
const sdkVersion = lazy(() => parseInt(device.sdkVersion));
38+
3139
const modalMap = new Map<number, DialogOptions>();
3240

3341
let TouchListener: TouchListener;
@@ -255,6 +263,8 @@ export class View extends ViewCommon {
255263
private layoutChangeListener: android.view.View.OnLayoutChangeListener;
256264
private _manager: android.support.v4.app.FragmentManager;
257265
private _rootManager: android.support.v4.app.FragmentManager;
266+
private _originalElevation: number;
267+
private _originalStateListAnimator: any; /* android.animation.StateListAnimator; */
258268

259269
nativeViewProtected: android.view.View;
260270

@@ -709,6 +719,74 @@ export class View extends ViewCommon {
709719
this.nativeViewProtected.setAlpha(float(value));
710720
}
711721

722+
[androidElevationProperty.getDefault](): number {
723+
if (sdkVersion() < 21) {
724+
return 0;
725+
}
726+
727+
// NOTE: overriden in Button implementation as for widgets with StateListAnimator (Button)
728+
// nativeView.getElevation() returns 0 at the time of the getDefault() query
729+
return layout.toDeviceIndependentPixels((<any>this.nativeViewProtected).getElevation());
730+
}
731+
[androidElevationProperty.setNative](value: number) {
732+
if (sdkVersion() < 21) {
733+
return;
734+
}
735+
736+
this.refreshStateListAnimator();
737+
}
738+
739+
[androidDynamicElevationOffsetProperty.getDefault](): number {
740+
return 0;
741+
}
742+
[androidDynamicElevationOffsetProperty.setNative](value: number) {
743+
if (sdkVersion() < 21) {
744+
return;
745+
}
746+
747+
this.refreshStateListAnimator();
748+
}
749+
750+
private refreshStateListAnimator() {
751+
const nativeView: any = this.nativeViewProtected;
752+
753+
const ObjectAnimator = android.animation.ObjectAnimator;
754+
const AnimatorSet = android.animation.AnimatorSet;
755+
756+
const duration = nativeView.getContext().getResources().getInteger(shortAnimTime) / 2;
757+
const elevation = layout.toDevicePixels(this.androidElevation || 0);
758+
const z = layout.toDevicePixels(0);
759+
const pressedZ = layout.toDevicePixels(this.androidDynamicElevationOffset || 0);
760+
761+
const pressedSet = new AnimatorSet();
762+
pressedSet.playTogether(java.util.Arrays.asList([
763+
ObjectAnimator.ofFloat(nativeView, "translationZ", [pressedZ])
764+
.setDuration(duration),
765+
ObjectAnimator.ofFloat(nativeView, "elevation", [elevation])
766+
.setDuration(0),
767+
]));
768+
769+
const notPressedSet = new AnimatorSet();
770+
notPressedSet.playTogether(java.util.Arrays.asList([
771+
ObjectAnimator.ofFloat(nativeView, "translationZ", [z])
772+
.setDuration(duration),
773+
ObjectAnimator.ofFloat(nativeView, "elevation", [elevation])
774+
.setDuration(0),
775+
]));
776+
777+
const defaultSet = new AnimatorSet();
778+
defaultSet.playTogether(java.util.Arrays.asList([
779+
ObjectAnimator.ofFloat(nativeView, "translationZ", [0]).setDuration(0),
780+
ObjectAnimator.ofFloat(nativeView, "elevation", [0]).setDuration(0),
781+
]));
782+
783+
const stateListAnimator = new (<any>android.animation).StateListAnimator();
784+
stateListAnimator.addState([statePressed, stateEnabled], pressedSet);
785+
stateListAnimator.addState([stateEnabled], notPressedSet);
786+
stateListAnimator.addState([], defaultSet);
787+
nativeView.setStateListAnimator(stateListAnimator);
788+
}
789+
712790
[horizontalAlignmentProperty.getDefault](): HorizontalAlignment {
713791
return <HorizontalAlignment>org.nativescript.widgets.ViewHelper.getHorizontalAlignment(this.nativeViewProtected);
714792
}
@@ -946,7 +1024,7 @@ function createNativePercentLengthProperty(options: NativePercentLengthPropertyO
9461024
const { getter, setter, auto = 0 } = options;
9471025
let setPixels, getPixels, setPercent;
9481026
if (getter) {
949-
View.prototype[getter] = function (this: View): PercentLength {
1027+
View.prototype[getter] = function(this: View): PercentLength {
9501028
if (options) {
9511029
setPixels = options.setPixels;
9521030
getPixels = options.getPixels;
@@ -962,7 +1040,7 @@ function createNativePercentLengthProperty(options: NativePercentLengthPropertyO
9621040
}
9631041
}
9641042
if (setter) {
965-
View.prototype[setter] = function (this: View, length: PercentLength) {
1043+
View.prototype[setter] = function(this: View, length: PercentLength) {
9661044
if (options) {
9671045
setPixels = options.setPixels;
9681046
getPixels = options.getPixels;

tns-core-modules/ui/core/view/view.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,16 @@ export abstract class View extends ViewBase {
224224
*/
225225
color: Color;
226226

227+
/**
228+
* Gets or sets the elevation of the android view.
229+
*/
230+
androidElevation: number;
231+
232+
/**
233+
* Gets or sets the dynamic elevation offset of the android view.
234+
*/
235+
androidDynamicElevationOffset: number;
236+
227237
/**
228238
* Gets or sets the background style property.
229239
*/

tns-core-modules/ui/styling/style-properties.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,3 +1099,9 @@ export const visibilityProperty = new CssProperty<Style, Visibility>({
10991099
}
11001100
});
11011101
visibilityProperty.register(Style);
1102+
1103+
export const androidElevationProperty = new CssProperty<Style, number>({ name: "androidElevation", cssName: "android-elevation", valueConverter: parseFloat });
1104+
androidElevationProperty.register(Style);
1105+
1106+
export const androidDynamicElevationOffsetProperty = new CssProperty<Style, number>({ name: "androidDynamicElevationOffset", cssName: "android-dynamic-elevation-offset", valueConverter: parseFloat });
1107+
androidDynamicElevationOffsetProperty.register(Style);

0 commit comments

Comments
 (0)