mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
feat(android): elevation shadow support (#7136)
This commit is contained in:
committed by
Manol Donev
parent
f8754913c3
commit
cf533a7b6d
43
apps/app/ui-tests-app/css/elevation.css
Normal file
43
apps/app/ui-tests-app/css/elevation.css
Normal file
@@ -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;
|
||||
}
|
||||
20
apps/app/ui-tests-app/css/elevation.ts
Normal file
20
apps/app/ui-tests-app/css/elevation.ts
Normal file
@@ -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;
|
||||
}
|
||||
25
apps/app/ui-tests-app/css/elevation.xml
Normal file
25
apps/app/ui-tests-app/css/elevation.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<Page xmlns="http://schemas.nativescript.org/tns.xsd" loaded="onLoaded">
|
||||
<ScrollView>
|
||||
<StackLayout backgroundColor="aliceblue" androidElevation="2" padding="10" margin="20">
|
||||
<Button text="default elevation" tap="buttonTap"/>
|
||||
|
||||
<Button class="elevation-0" text="elevation 0"/>
|
||||
|
||||
<Label class="elevation-2" text="Label with elevation 2"/>
|
||||
|
||||
<StackLayout androidElevation="4" backgroundColor="#ff1744" horizontalAlignment="center" margin="20">
|
||||
<Button class="elevation-2" text="elevation 2"/>
|
||||
<Button class="elevation-4" text="elevation 4"/>
|
||||
</StackLayout>
|
||||
|
||||
<StackLayout orientation="horizontal" horizontalAlignment="center">
|
||||
<Button class="round" androidElevation="4" text="el. 4"/>
|
||||
<Button class="elevation-8 round" text="el. 8"/>
|
||||
</StackLayout>
|
||||
|
||||
<Label class="elevation-8" text="elevation 8"/>
|
||||
<Button class="elevation-8" text="elevation 8, with radius" borderRadius="4"/>
|
||||
<Button class="elevation-10" text="elevation 10"/>
|
||||
</StackLayout>
|
||||
</ScrollView>
|
||||
</Page>
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1018,7 +1018,7 @@ export const classNameProperty = new Property<ViewBase, string>({
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<number, DialogOptions>();
|
||||
|
||||
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((<any>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 (<any>android.animation).StateListAnimator();
|
||||
stateListAnimator.addState([statePressed, stateEnabled], pressedSet);
|
||||
stateListAnimator.addState([stateEnabled], notPressedSet);
|
||||
stateListAnimator.addState([], defaultSet);
|
||||
nativeView.setStateListAnimator(stateListAnimator);
|
||||
}
|
||||
|
||||
[horizontalAlignmentProperty.getDefault](): HorizontalAlignment {
|
||||
return <HorizontalAlignment>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;
|
||||
|
||||
10
tns-core-modules/ui/core/view/view.d.ts
vendored
10
tns-core-modules/ui/core/view/view.d.ts
vendored
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -1099,3 +1099,9 @@ export const visibilityProperty = new CssProperty<Style, Visibility>({
|
||||
}
|
||||
});
|
||||
visibilityProperty.register(Style);
|
||||
|
||||
export const androidElevationProperty = new CssProperty<Style, number>({ name: "androidElevation", cssName: "android-elevation", valueConverter: parseFloat });
|
||||
androidElevationProperty.register(Style);
|
||||
|
||||
export const androidDynamicElevationOffsetProperty = new CssProperty<Style, number>({ name: "androidDynamicElevationOffset", cssName: "android-dynamic-elevation-offset", valueConverter: parseFloat });
|
||||
androidDynamicElevationOffsetProperty.register(Style);
|
||||
|
||||
8
tns-core-modules/ui/styling/style/style.d.ts
vendored
8
tns-core-modules/ui/styling/style/style.d.ts
vendored
@@ -92,6 +92,8 @@ export class Style extends Observable {
|
||||
public fontWeight: FontWeight;
|
||||
public font: string;
|
||||
|
||||
public androidElevation: number;
|
||||
public androidDynamicElevationOffset: number;
|
||||
public zIndex: number;
|
||||
public opacity: number;
|
||||
public visibility: Visibility;
|
||||
@@ -127,13 +129,13 @@ export class Style extends Observable {
|
||||
public selectedTabTextColor: Color;
|
||||
public androidSelectedTabHighlightColor: Color;
|
||||
|
||||
// ListView-specific props
|
||||
// ListView-specific props
|
||||
public separatorColor: Color;
|
||||
|
||||
//SegmentedBar-specific props
|
||||
public selectedBackgroundColor: Color;
|
||||
|
||||
// Page-specific props
|
||||
// Page-specific props
|
||||
public statusBarStyle: "light" | "dark";
|
||||
public androidStatusBarBackground: Color;
|
||||
|
||||
@@ -167,4 +169,4 @@ interface PropertyBagClass {
|
||||
}
|
||||
interface PropertyBag {
|
||||
[property: string]: string;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ export class Style extends Observable implements StyleDefinition {
|
||||
public fontWeight: FontWeight;
|
||||
public font: string;
|
||||
|
||||
public androidElevation: number;
|
||||
public androidDynamicElevationOffset: number;
|
||||
public zIndex: number;
|
||||
public opacity: number;
|
||||
public visibility: Visibility;
|
||||
@@ -100,13 +102,13 @@ export class Style extends Observable implements StyleDefinition {
|
||||
public selectedTabTextColor: Color;
|
||||
public androidSelectedTabHighlightColor: Color;
|
||||
|
||||
// ListView-specific props
|
||||
// ListView-specific props
|
||||
public separatorColor: Color;
|
||||
|
||||
//SegmentedBar-specific props
|
||||
public selectedBackgroundColor: Color;
|
||||
|
||||
// Page-specific props
|
||||
// Page-specific props
|
||||
public statusBarStyle: "light" | "dark";
|
||||
public androidStatusBarBackground: Color;
|
||||
|
||||
@@ -124,4 +126,4 @@ export class Style extends Observable implements StyleDefinition {
|
||||
|
||||
public PropertyBag: { new(): { [property: string]: string }, prototype: { [property: string]: string } };
|
||||
}
|
||||
Style.prototype.PropertyBag = class { [property: string]: string; }
|
||||
Style.prototype.PropertyBag = class { [property: string]: string; }
|
||||
|
||||
Reference in New Issue
Block a user