diff --git a/apps/toolbox/src/pages/box-shadow.ts b/apps/toolbox/src/pages/box-shadow.ts index 9815bc6e2..0414d6d4e 100644 --- a/apps/toolbox/src/pages/box-shadow.ts +++ b/apps/toolbox/src/pages/box-shadow.ts @@ -7,11 +7,13 @@ export function navigatingTo(args: EventData) { } export class BoxShadowModel extends Observable { - private _selectedComponentType: string; + private _selectedComponentType: string = 'buttons'; private _selectedBackgroundType: string; private _selectedBorderType: string; private _selectedAnimation: string; - private _boxShadow: string = '5 5 1 1 rgba(255, 0, 0, .9)'; + private _boxShadow: string = '0 10 15 -3 rgba(200, 0, 0, 0.4)'; + // private _boxShadow: string = '5 5 1 1 rgba(255, 0, 0, .9)'; + // private _boxShadow: string = '5 5 5 10 rgba(255, 0, 0, .9)'; background: string; borderWidth: number; diff --git a/apps/toolbox/src/pages/box-shadow.xml b/apps/toolbox/src/pages/box-shadow.xml index d1d373a95..f19aea34b 100644 --- a/apps/toolbox/src/pages/box-shadow.xml +++ b/apps/toolbox/src/pages/box-shadow.xml @@ -5,10 +5,10 @@ - + - + - - - - - - diff --git a/packages/core/platforms/android/widgets-release.aar b/packages/core/platforms/android/widgets-release.aar index eb76359b6..8e0b8af9e 100644 Binary files a/packages/core/platforms/android/widgets-release.aar and b/packages/core/platforms/android/widgets-release.aar differ diff --git a/packages/core/ui/styling/background.android.ts b/packages/core/ui/styling/background.android.ts index 9c38117e7..73bce2383 100644 --- a/packages/core/ui/styling/background.android.ts +++ b/packages/core/ui/styling/background.android.ts @@ -6,9 +6,8 @@ import { parse } from '../../css-value'; import { path, knownFolders } from '../../file-system'; import * as application from '../../application'; import { profile } from '../../profiling'; -import { Color } from '../../color'; -import { Screen } from '../../platform'; import { CSSShadow } from './css-shadow'; +import { Length, LengthType } from './style-properties'; export * from './background-common'; interface AndroidView { @@ -28,8 +27,12 @@ export namespace ad { } function isSetColorFilterOnlyWidget(nativeView: android.view.View): boolean { + // prettier-ignore return ( - nativeView instanceof android.widget.Button || (nativeView instanceof androidx.appcompat.widget.Toolbar && getSDK() >= 21) // There is an issue with the DrawableContainer which was fixed for API version 21 and above: https://code.google.com/p/android/issues/detail?id=60183 + nativeView instanceof android.widget.Button + || (nativeView instanceof androidx.appcompat.widget.Toolbar && getSDK() >= 21) + // There is an issue with the DrawableContainer which was fixed + // for API version 21 and above: https://code.google.com/p/android/issues/detail?id=60183 ); } @@ -48,7 +51,15 @@ export namespace ad { androidView._cachedDrawable = constantState || drawable; } const isBorderDrawable = drawable instanceof org.nativescript.widgets.BorderDrawable; - const onlyColor = !background.hasBorderWidth() && !background.hasBorderRadius() && !background.clipPath && !background.image && !!background.color; + + // prettier-ignore + const onlyColor = !background.hasBorderWidth() + && !background.hasBorderRadius() + && !background.hasBoxShadow() + && !background.clipPath + && !background.image + && !!background.color; + if (!isBorderDrawable && drawable instanceof android.graphics.drawable.ColorDrawable && onlyColor) { drawable.setColor(background.color.android); drawable.invalidateSelf(); @@ -71,13 +82,19 @@ export namespace ad { // this is the fastest way to change only background color nativeView.setBackgroundColor(background.color.android); } else if (!background.isEmpty()) { - let backgroundDrawable = drawable as org.nativescript.widgets.BorderDrawable; - if (!isBorderDrawable) { + let backgroundDrawable = drawable; + + if (drawable instanceof org.nativescript.widgets.BoxShadowDrawable) { + // if we have BoxShadow's we have to get the underlying drawable + backgroundDrawable = drawable.getWrappedDrawable(); + } + + if (backgroundDrawable instanceof org.nativescript.widgets.BorderDrawable) { + refreshBorderDrawable(view, backgroundDrawable); + } else { backgroundDrawable = new org.nativescript.widgets.BorderDrawable(layout.getDisplayDensity(), view.toString()); refreshBorderDrawable(view, backgroundDrawable); nativeView.setBackground(backgroundDrawable); - } else { - refreshBorderDrawable(view, backgroundDrawable); } } else { const cachedDrawable = androidView._cachedDrawable; @@ -228,24 +245,19 @@ function createNativeCSSValueArray(css: string): androidNative.Array { application.android.on('activityStarted', (args) => { 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 006646d57..53f51e64d 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 @@ -2,9 +2,16 @@ module nativescript { module widgets { - export class Utils { - public static drawBoxShadow(view: android.view.View, value: string); - } + export class Utils { + public static drawBoxShadow(view: android.view.View, value: string): void; + public static clearBoxShadow(view: android.view.View): void; + } + + export class BoxShadowDrawable { + public constructor(drawable: android.graphics.drawable.Drawable, value: string); + public getWrappedDrawable(): android.graphics.drawable.Drawable; + public toString(): string; + } export class CustomTransition extends androidx.transition.Visibility { constructor(animatorSet: android.animation.AnimatorSet, transitionName: string); diff --git a/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BoxShadowDrawable.java b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BoxShadowDrawable.java new file mode 100644 index 000000000..584572fc6 --- /dev/null +++ b/packages/ui-mobile-base/android/widgets/src/main/java/org/nativescript/widgets/BoxShadowDrawable.java @@ -0,0 +1,154 @@ +package org.nativescript.widgets; + +import android.graphics.BlurMaskFilter; +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RectShape; +import android.graphics.drawable.shapes.RoundRectShape; +import android.os.Build; +import android.util.Log; + +import androidx.annotation.RequiresApi; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Arrays; + +@RequiresApi(api = Build.VERSION_CODES.M) +public class BoxShadowDrawable extends LayerDrawable { + // Static parameters + protected final static int DEFAULT_BACKGROUND_COLOR = Color.WHITE; + protected final static String TAG = "BoxShadowDrawable"; + + // BoxShadow Parameters + protected int offsetX = 0; + protected int offsetY = 0; + protected int blurRadius = 0; + protected int spreadRadius = 0; + protected int shadowColor = Color.BLACK; + + // Layers + protected final ShapeDrawable shadowLayer; + protected final ShapeDrawable overlayLayer; + protected final Drawable wrappedLayer; + + protected float[] currentCornerRadii; + + public BoxShadowDrawable(Drawable wrappedDrawable, String value) { + super(new Drawable[]{}); + + Log.d(TAG, "Constructing BoxShadowDrawable!"); + + this.shadowLayer = new ShapeDrawable(new RectShape()); + this.overlayLayer = this.createOverlayLayer(); + this.wrappedLayer = wrappedDrawable; + + // add our layers + this.addLayer(shadowLayer); + this.addLayer(overlayLayer); + this.addLayer(wrappedLayer); + + this.setValue(value); + } + + // to allow applying any bg changes on original Drawable + public Drawable getWrappedDrawable() { + return this.wrappedLayer; + } + + public void setValue(String value) { + try { + JSONObject config = new JSONObject(value); + offsetX = config.getInt("offsetX"); + offsetY = config.getInt("offsetY"); + blurRadius = config.getInt("blurRadius"); + spreadRadius = config.getInt("spreadRadius"); + shadowColor = config.getInt("shadowColor"); + + float[] outerRadius; + + // if we are wrapping a BorderDrawable - we can get the radii from it + if(wrappedLayer instanceof BorderDrawable) { + BorderDrawable b = (BorderDrawable) wrappedLayer; + outerRadius = new float[]{ + b.getBorderTopLeftRadius(), + b.getBorderTopLeftRadius(), + + b.getBorderTopRightRadius(), + b.getBorderTopRightRadius(), + + b.getBorderBottomRightRadius(), + b.getBorderBottomRightRadius(), + + b.getBorderBottomLeftRadius(), + b.getBorderBottomLeftRadius(), + }; + } else { + int cornerRadius = 0; + try { + cornerRadius = config.getInt("cornerRadius"); + } catch (JSONException ignore) {} + + outerRadius = new float[8]; + Arrays.fill(outerRadius, cornerRadius); + } + + if(!Arrays.equals(outerRadius, currentCornerRadii)) { + Log.d(TAG, "Update layer shape"); + shadowLayer.setShape(new RoundRectShape(outerRadius, null, null)); + overlayLayer.setShape(new RoundRectShape(outerRadius, null, null)); + + // update current + currentCornerRadii = outerRadius; + } + + // apply new shadow parameters + this.applyShadow(); + } catch (JSONException exception) { + Log.d(TAG, "Caught JSONException..."); + exception.printStackTrace(); + } + } + + private void applyShadow() { + Log.d(TAG, "applyShadow: " + this); + + // apply boxShadow + shadowLayer.getPaint().setColor(shadowColor); + shadowLayer.getPaint().setMaskFilter(new BlurMaskFilter( + Float.MIN_VALUE + blurRadius, + BlurMaskFilter.Blur.NORMAL + )); + shadowLayer.getPaint().setAntiAlias(true); + + // apply insets that mimic offsets/spread to the shadowLayer + int inset = -spreadRadius; + Log.d(TAG, "Insets:" + + "\n l: " + (inset + offsetX) + + "\n t: " + (inset + offsetY) + + "\n r: " + (inset - offsetX) + + "\n b: " + (inset - offsetY) + ); + this.setLayerInset(0, + inset + offsetX, + inset + offsetY, + inset - offsetX, + inset - offsetY + ); + } + + private ShapeDrawable createOverlayLayer() { + ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape()); + shapeDrawable.getPaint().setColor(DEFAULT_BACKGROUND_COLOR); + + return shapeDrawable; + } + + @Override + public String toString() { + return "BoxShadowDrawable { oX:" + offsetX + " oY:" + offsetY + " br:" + blurRadius + " sr:" + spreadRadius + " c:" + shadowColor + " }"; + } +} 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 index d98350b60..7b2f421f6 100644 --- 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 @@ -1,109 +1,33 @@ -/** - * - */ package org.nativescript.widgets; -/** - * @author triniwiz - */ - import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; -import android.graphics.drawable.LayerDrawable; -import android.graphics.drawable.ShapeDrawable; -import android.graphics.drawable.shapes.RoundRectShape; +import android.util.Log; 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"); + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) { + return; + } + Log.d("BoxShadowDrawable", "drawBoxShadow"); + Drawable wrap = view.getBackground(); + if(wrap == null) { + wrap = new ColorDrawable(Color.TRANSPARENT); + } else if(wrap instanceof BoxShadowDrawable) { + wrap = ((BoxShadowDrawable) view.getBackground()).getWrappedDrawable(); + Log.d("BoxShadowDrawable", "already a BoxShadowDrawable, getting wrapped drawable:" + wrap.getClass().getName()); + } - float cornerRadiusValue = cornerRadius * scale; + // replace background + Log.d("BoxShadowDrawable", "replacing background with new BoxShadowDrawable..."); + view.setBackground(new BoxShadowDrawable(wrap, value)); - 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) { + int count = 0; + while (view.getParent() != null && view.getParent() instanceof ViewGroup) { count++; ViewGroup parent = (ViewGroup) view.getParent(); parent.setClipChildren(false); @@ -112,11 +36,20 @@ public class Utils { if (count == 1) { break; } - nativeView = parent; } + } - nativeView.setBackground(drawable); - } catch (JSONException ignore) { + public static void clearBoxShadow(View view) { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) { + return; + } + Log.d("BoxShadowDrawable", "clearBoxShadow."); + + Drawable bg = view.getBackground(); + if(bg instanceof BoxShadowDrawable) { + Drawable original = ((BoxShadowDrawable) view.getBackground()).getWrappedDrawable(); + Log.d("BoxShadowDrawable", "BoxShadowDrawable found, resetting to original: " + original.getClass().getName()); + view.setBackground(original); } } }