diff --git a/apps/toolbox/src/app.css b/apps/toolbox/src/app.css index 28e7dab40..47446618d 100644 --- a/apps/toolbox/src/app.css +++ b/apps/toolbox/src/app.css @@ -7,12 +7,31 @@ components that have the btn class name. */ .btn { font-size: 18; + /* box-shadow: -5 -5 10 10 navy; */ + box-shadow: -5 -5 rgba(0,0,0,0.5); + background-color: #add8e6; + color: navy; + /* TODO: adding border radius breaks shadow */ + /* border-radius: 10; */ } .bold{ font-weight: bold; } +.sample-animation { + animation-name: rotate-expand; + animation-duration: 5s; + animation-iteration-count: infinite; +} + +@keyframes rotate-expand { + 0%, 50% { background-color: red; width: 200; transform: rotate(0) scale(1,1); } + 25%, 75% { background-color: yellow; width: 100; transform: rotate(180) scale(1.2,1.2); } + 100% { background-color: red; width: 200; transform: rotate(0) scale(1,1); } +} + + .icon-label{ font-size: 22; font-family: "ns-playground-font"; diff --git a/apps/toolbox/src/main-page.xml b/apps/toolbox/src/main-page.xml index 92d262114..8d351e81c 100644 --- a/apps/toolbox/src/main-page.xml +++ b/apps/toolbox/src/main-page.xml @@ -3,18 +3,49 @@ + - + diff --git a/apps/toolbox/src/main-view-model.ts b/apps/toolbox/src/main-view-model.ts index e21219ebb..f69e5f985 100644 --- a/apps/toolbox/src/main-view-model.ts +++ b/apps/toolbox/src/main-view-model.ts @@ -1,4 +1,4 @@ -import { Observable, Frame } from '@nativescript/core'; +import { Observable, Frame, StackLayout } from '@nativescript/core'; export class HelloWorldModel extends Observable { private _counter: number; @@ -23,6 +23,15 @@ export class HelloWorldModel extends Observable { } } + toggleAnimation(args) { + const layout = args.object as StackLayout; + if (!layout.className) { + layout.className = 'sample-animation'; + } else { + layout.className = undefined; + } + } + onTap() { this._counter--; this.updateMessage(); diff --git a/packages/core/ui/core/view/index.d.ts b/packages/core/ui/core/view/index.d.ts index 4ddd4c1ca..e546e6b88 100644 --- a/packages/core/ui/core/view/index.d.ts +++ b/packages/core/ui/core/view/index.d.ts @@ -6,6 +6,7 @@ import { Animation, AnimationDefinition, AnimationPromise } from '../../animatio import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from '../../styling/style-properties'; import { GestureTypes, GestureEventData, GesturesObserver } from '../../gestures'; import { LinearGradient } from '../../styling/gradient'; +import { BoxShadow } from '../../styling/box-shadow'; // helpers (these are okay re-exported here) export * from './view-helper'; @@ -250,6 +251,11 @@ export abstract class View extends ViewBase { */ backgroundImage: string | LinearGradient; + /** + * Gets or sets the box shadow of the view. + */ + boxShadow: string | BoxShadow; + /** * Gets or sets the minimum width the view may grow to. */ diff --git a/packages/core/ui/core/view/view-common.ts b/packages/core/ui/core/view/view-common.ts index f5178100b..511025e54 100644 --- a/packages/core/ui/core/view/view-common.ts +++ b/packages/core/ui/core/view/view-common.ts @@ -22,6 +22,7 @@ import { LinearGradient } from '../../styling/linear-gradient'; import { TextTransform } from '../../text-base'; import * as am from '../../animation'; +import { BoxShadow } from '../../styling/box-shadow'; // helpers (these are okay re-exported here) export * from './view-helper'; @@ -581,6 +582,13 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition { this.style.backgroundRepeat = value; } + get boxShadow(): BoxShadow { + return this.style.boxShadow; + } + set boxShadow(value: BoxShadow) { + this.style.boxShadow = value; + } + get minWidth(): Length { return this.style.minWidth; } diff --git a/packages/core/ui/layouts/layout-base.ios.ts b/packages/core/ui/layouts/layout-base.ios.ts index a7eb4205f..fc72c7c57 100644 --- a/packages/core/ui/layouts/layout-base.ios.ts +++ b/packages/core/ui/layouts/layout-base.ios.ts @@ -23,6 +23,7 @@ export class LayoutBase extends LayoutBaseCommon { _setNativeClipToBounds() { if (this.clipToBounds) { + // TODO: temporarily setting this to false as it crops the shadow this.nativeViewProtected.clipsToBounds = true; } else { super._setNativeClipToBounds(); diff --git a/packages/core/ui/styling/background.android.ts b/packages/core/ui/styling/background.android.ts index 65459fdb9..67cbbc5dc 100644 --- a/packages/core/ui/styling/background.android.ts +++ b/packages/core/ui/styling/background.android.ts @@ -6,6 +6,9 @@ import { parse } from '../../css-value'; import { path, knownFolders } from '../../file-system'; import * as application from '../../application'; import { profile } from '../../profiling'; +import { BoxShadow } from './box-shadow'; +import { Color } from '../../color'; +import { Screen } from '../../platform'; export * from './background-common'; interface AndroidView { @@ -90,6 +93,11 @@ export namespace ad { nativeView.setBackground(defaultDrawable); } + const boxShadow = view.style.boxShadow; + if (boxShadow) { + drawBoxShadow(nativeView, boxShadow); + } + // TODO: Can we move BorderWidths as separate native setter? // This way we could skip setPadding if borderWidth is not changed. const leftPadding = Math.ceil(view.effectiveBorderLeftWidth + view.effectivePaddingLeft); @@ -218,6 +226,24 @@ function createNativeCSSValueArray(css: string): native.Array -1) { + arr = value.split(' '); + colorRaw = arr.pop(); + } else { + arr = value.split(/[ ,]+/); + colorRaw = arr.pop(); + } + + let offsetX: number; + let offsetY: number; + let blurRadius: number; // not currently in use + let spreadRadius: number; // maybe rename this to just radius + let color: Color = new Color(colorRaw); + + if (arr.length === 2) { + offsetX = parseFloat(arr[0]); + offsetY = parseFloat(arr[1]); + } else if (arr.length === 3) { + offsetX = parseFloat(arr[0]); + offsetY = parseFloat(arr[1]); + blurRadius = parseFloat(arr[2]); + } else if (arr.length === 4) { + offsetX = parseFloat(arr[0]); + offsetY = parseFloat(arr[1]); + blurRadius = parseFloat(arr[2]); + spreadRadius = parseFloat(arr[3]); + } else { + throw new Error('Expected 3, 4 or 5 parameters. Actual: ' + value); + } + return { + offsetX: offsetX, + offsetY: offsetY, + blurRadius: blurRadius, + spreadRadius: spreadRadius, + color: color, + }; + } else { + return value; + } +} + interface Thickness { top: string; right: string; @@ -1275,6 +1321,18 @@ export const borderBottomLeftRadiusProperty = new CssProperty({ }); borderBottomLeftRadiusProperty.register(Style); +const boxShadowProperty = new CssProperty({ + name: 'boxShadow', + cssName: 'box-shadow', + valueChanged: (target, oldValue, newValue) => { + target.boxShadow = newValue; + }, + valueConverter: (value) => { + return parseBoxShadowProperites(value); + }, +}); +boxShadowProperty.register(Style); + function isNonNegativeFiniteNumber(value: number): boolean { return isFinite(value) && !isNaN(value) && value >= 0; } diff --git a/packages/core/ui/styling/style/index.ts b/packages/core/ui/styling/style/index.ts index 3ae86e7c9..41d706662 100644 --- a/packages/core/ui/styling/style/index.ts +++ b/packages/core/ui/styling/style/index.ts @@ -11,6 +11,7 @@ import { Observable } from '../../../data/observable'; import { FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent, Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf } from '../../layouts/flexbox-layout'; import { Trace } from '../../../trace'; import { TextAlignment, TextDecoration, TextTransform, WhiteSpace } from '../../text-base'; +import { BoxShadow } from '../box-shadow'; export interface CommonLayoutParams { width: number; @@ -137,6 +138,8 @@ export class Style extends Observable implements StyleDefinition { public borderBottomRightRadius: Length; public borderBottomLeftRadius: Length; + public boxShadow: BoxShadow; + public fontSize: number; public fontFamily: string; public fontStyle: FontStyle; diff --git a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts index d2ea0f51b..006646d57 100644 --- a/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts +++ b/packages/types-android/src/lib/android/org.nativescript.widgets.d.ts @@ -1,6 +1,11 @@ declare module org { module nativescript { module widgets { + + export class Utils { + public static drawBoxShadow(view: android.view.View, value: string); + } + export class CustomTransition extends androidx.transition.Visibility { constructor(animatorSet: android.animation.AnimatorSet, transitionName: string); public setResetOnTransitionEnd(resetOnTransitionEnd: boolean): void; diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java new file mode 100644 index 000000000..d98350b60 --- /dev/null +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/Utils.java @@ -0,0 +1,122 @@ +/** + * + */ +package org.nativescript.widgets; + +/** + * @author triniwiz + */ + +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.view.View; +import android.view.ViewGroup; + +import org.json.JSONException; +import org.json.JSONObject; + +public class Utils { + public static void drawBoxShadow(View view, String value) { + try { + JSONObject config = new JSONObject(value); + int shadowColor = config.getInt("shadowColor"); + int cornerRadius = config.getInt("cornerRadius"); + int spreadRadius = config.getInt("spreadRadius"); + int blurRadius = config.getInt("blurRadius"); + int configOffsetX = config.getInt("offsetX"); + int configOffsetY = config.getInt("offsetY"); + int scale = config.getInt("scale"); + + + float cornerRadiusValue = cornerRadius * scale; + + float shadowSpread = spreadRadius * scale; + + // Set shadow layer + float[] outerRadius = {cornerRadiusValue, cornerRadiusValue, cornerRadiusValue, cornerRadiusValue, cornerRadiusValue, cornerRadiusValue, cornerRadiusValue, cornerRadiusValue}; + + // Default background for transparent/semi-transparent background so it doesn't see through the shadow + int defaultBackgroundColor = Color.WHITE; + RoundRectShape backgroundRectShape = new RoundRectShape(outerRadius, null, null); + ShapeDrawable backgroundDrawable = new ShapeDrawable(backgroundRectShape); + backgroundDrawable.getPaint().setColor(defaultBackgroundColor); + + // shadow layer setup + RoundRectShape shadowRectShape = new RoundRectShape(outerRadius, null, null); + ShapeDrawable shadowShapeDrawable = new ShapeDrawable(shadowRectShape); + shadowShapeDrawable.getPaint().setShadowLayer(shadowSpread, 0, 0, shadowColor); + shadowShapeDrawable.getPaint().setAntiAlias(true); + + // set shadow direction + Drawable[] drawableArray = new Drawable[3]; + drawableArray[0] = shadowShapeDrawable; + drawableArray[1] = backgroundDrawable; + drawableArray[2] = view.getBackground(); + LayerDrawable drawable = new LayerDrawable(drawableArray); + + // workaround to show shadow offset (similar to ios's offsets) + int shadowInsetsLeft; + int shadowInsetsTop; + int shadowInsetsRight; + int shadowInsetsBottom; + + float offsetX = configOffsetX - spreadRadius; + // ignore the following line, this is similar to the adjustedShadowOffset on ios. + // it is just used to experiment the amount of insets that need to be applied based + // on the offset provided. Need to use some real calculation to gain parity (ask Osei) + float insetScaleFactor = 4f / 5f; + + if (configOffsetX == 0) { + shadowInsetsLeft = 0; + shadowInsetsRight = 0; + } else if (configOffsetX > 0) { + shadowInsetsLeft = (int) (shadowSpread * insetScaleFactor); + shadowInsetsRight = (int) ((offsetX < 0 ? 0 : offsetX) * scale * insetScaleFactor); + } else { + shadowInsetsLeft = (int) ((offsetX < 0 ? 0 : offsetX) * scale * insetScaleFactor); + shadowInsetsRight = (int) (shadowSpread * insetScaleFactor); + } + float offsetY = configOffsetY - spreadRadius; + if (configOffsetY == 0) { + shadowInsetsTop = 0; + shadowInsetsBottom = 0; + } else if (configOffsetY >= 0) { + shadowInsetsTop = (int) (shadowSpread * insetScaleFactor); + shadowInsetsBottom = (int) ((offsetY < 0 ? 0 : offsetY) * scale * insetScaleFactor); + } else { + shadowInsetsTop = (int) ((offsetY < 0 ? 0 : offsetY) * scale * insetScaleFactor); + shadowInsetsBottom = (int) (shadowSpread * insetScaleFactor); + } + + // TODO: this isn't really a shadow offset per se, but just having the some layer + // drawable layer have an inset to mimic an offset (feels very hacky ugh) + drawable.setLayerInset(0, shadowInsetsLeft, shadowInsetsTop, shadowInsetsRight, shadowInsetsBottom); + + // this is what it shadows look like without offsets - uncomment the following line, + // and comment out line above to see what the shadow without any inset modification looks like + // on android + // drawable.setLayerInset(0, shadowSpread, shadowSpread, shadowSpread, shadowSpread); + + // make sure parent doesn't clip the shadows + int count = 0; + View nativeView = view; + while (view.getParent() != null && view.getParent() instanceof ViewGroup) { + count++; + ViewGroup parent = (ViewGroup) view.getParent(); + parent.setClipChildren(false); + parent.setClipToPadding(false); + // removing clipping from all breaks the ui + if (count == 1) { + break; + } + nativeView = parent; + } + + nativeView.setBackground(drawable); + } catch (JSONException ignore) { + } + } +}