feat: implement BoxShadowDrawable

This commit is contained in:
Igor Randjelovic
2021-03-15 18:08:12 +01:00
committed by Nathan Walker
parent a822f2affb
commit 9a7d3ecb34
7 changed files with 234 additions and 142 deletions

View File

@ -7,11 +7,13 @@ export function navigatingTo(args: EventData) {
} }
export class BoxShadowModel extends Observable { export class BoxShadowModel extends Observable {
private _selectedComponentType: string; private _selectedComponentType: string = 'buttons';
private _selectedBackgroundType: string; private _selectedBackgroundType: string;
private _selectedBorderType: string; private _selectedBorderType: string;
private _selectedAnimation: 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; background: string;
borderWidth: number; borderWidth: number;

View File

@ -5,10 +5,10 @@
</Page.actionBar> </Page.actionBar>
<GridLayout rows="*, auto, *" class="box-shadow-demo"> <GridLayout rows="*, auto, *" class="box-shadow-demo">
<StackLayout backgroundColor="#ededed" row="0" padding="20" id="boxShadowDemo"> <StackLayout backgroundColor="#ededed" row="0" id="boxShadowDemo">
<!-- layouts --> <!-- layouts -->
<ScrollView height="100%" visibility="{{ selectedComponentType === 'layouts' ? 'visible' : 'collapsed' }}"> <ScrollView height="100%" visibility="{{ selectedComponentType === 'layouts' ? 'visible' : 'collapsed' }}">
<StackLayout> <StackLayout padding="20">
<StackLayout <StackLayout
width="300" width="300"
height="100" height="100"
@ -74,22 +74,6 @@
<Label text="FlexboxLayout"></Label> <Label text="FlexboxLayout"></Label>
</FlexboxLayout> </FlexboxLayout>
<GridLayout
width="300"
height="100"
padding="4"
boxShadow="{{ appliedBoxShadow }}"
tap="{{ toggleAnimation }}"
>
<StackLayout
borderWidth="4"
borderRadius="20"
backgroundColor="white"
>
<Label text="BorderRadius + BoxShadow on parent container"></Label>
</StackLayout>
</GridLayout>
</StackLayout> </StackLayout>
</ScrollView> </ScrollView>

View File

@ -6,9 +6,8 @@ import { parse } from '../../css-value';
import { path, knownFolders } from '../../file-system'; import { path, knownFolders } from '../../file-system';
import * as application from '../../application'; import * as application from '../../application';
import { profile } from '../../profiling'; import { profile } from '../../profiling';
import { Color } from '../../color';
import { Screen } from '../../platform';
import { CSSShadow } from './css-shadow'; import { CSSShadow } from './css-shadow';
import { Length, LengthType } from './style-properties';
export * from './background-common'; export * from './background-common';
interface AndroidView { interface AndroidView {
@ -28,8 +27,12 @@ export namespace ad {
} }
function isSetColorFilterOnlyWidget(nativeView: android.view.View): boolean { function isSetColorFilterOnlyWidget(nativeView: android.view.View): boolean {
// prettier-ignore
return ( 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; androidView._cachedDrawable = constantState || drawable;
} }
const isBorderDrawable = drawable instanceof org.nativescript.widgets.BorderDrawable; 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) { if (!isBorderDrawable && drawable instanceof android.graphics.drawable.ColorDrawable && onlyColor) {
drawable.setColor(background.color.android); drawable.setColor(background.color.android);
drawable.invalidateSelf(); drawable.invalidateSelf();
@ -71,13 +82,19 @@ export namespace ad {
// this is the fastest way to change only background color // this is the fastest way to change only background color
nativeView.setBackgroundColor(background.color.android); nativeView.setBackgroundColor(background.color.android);
} else if (!background.isEmpty()) { } else if (!background.isEmpty()) {
let backgroundDrawable = drawable as org.nativescript.widgets.BorderDrawable; let backgroundDrawable = drawable;
if (!isBorderDrawable) {
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()); backgroundDrawable = new org.nativescript.widgets.BorderDrawable(layout.getDisplayDensity(), view.toString());
refreshBorderDrawable(view, backgroundDrawable); refreshBorderDrawable(view, backgroundDrawable);
nativeView.setBackground(backgroundDrawable); nativeView.setBackground(backgroundDrawable);
} else {
refreshBorderDrawable(view, backgroundDrawable);
} }
} else { } else {
const cachedDrawable = androidView._cachedDrawable; const cachedDrawable = androidView._cachedDrawable;
@ -228,24 +245,19 @@ function createNativeCSSValueArray(css: string): androidNative.Array<org.natives
} }
function drawBoxShadow(nativeView: android.view.View, view: View, boxShadow: CSSShadow) { function drawBoxShadow(nativeView: android.view.View, view: View, boxShadow: CSSShadow) {
const color = boxShadow.color;
const shadowOpacity = color.a;
const shadowColor = new Color(shadowOpacity, color.r, color.g, color.b);
const cornerRadius = view.borderRadius; // this should be applied to the main view as well (try 20 with a transparent background on the xml to see the effect)
const config = { const config = {
shadowColor: shadowColor.android, shadowColor: boxShadow.color.android,
cornerRadius: cornerRadius, cornerRadius: Length.toDevicePixels(view.borderRadius as LengthType, 0.0),
spreadRadius: boxShadow.spreadRadius, spreadRadius: Length.toDevicePixels(boxShadow.spreadRadius, 0.0),
blurRadius: boxShadow.blurRadius, blurRadius: Length.toDevicePixels(boxShadow.blurRadius, 0.0),
offsetX: boxShadow.offsetX, offsetX: Length.toDevicePixels(boxShadow.offsetX, 0.0),
offsetY: boxShadow.offsetY, offsetY: Length.toDevicePixels(boxShadow.offsetY, 0.0),
scale: Screen.mainScreen.scale,
}; };
org.nativescript.widgets.Utils.drawBoxShadow(nativeView, JSON.stringify(config)); org.nativescript.widgets.Utils.drawBoxShadow(nativeView, JSON.stringify(config));
} }
function clearBoxShadow(nativeView: android.view.View) { function clearBoxShadow(nativeView: android.view.View) {
// org.nativescript.widgets.Utils.clearBoxShadow(nativeView); org.nativescript.widgets.Utils.clearBoxShadow(nativeView);
} }
export enum CacheMode { export enum CacheMode {
@ -279,13 +291,13 @@ export function initImageCache(context: android.content.Context, mode = CacheMod
imageFetcher.initCache(); imageFetcher.initCache();
} }
function onLivesync(args): void { function onLiveSync(args): void {
if (imageFetcher) { if (imageFetcher) {
imageFetcher.clearCache(); imageFetcher.clearCache();
} }
} }
global.NativeScriptGlobals.events.on('livesync', onLivesync); global.NativeScriptGlobals.events.on('livesync', onLiveSync);
global.NativeScriptGlobals.addEventWiring(() => { global.NativeScriptGlobals.addEventWiring(() => {
application.android.on('activityStarted', (args) => { application.android.on('activityStarted', (args) => {

View File

@ -2,9 +2,16 @@
module nativescript { module nativescript {
module widgets { module widgets {
export class Utils { export class Utils {
public static drawBoxShadow(view: android.view.View, value: string); 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 { export class CustomTransition extends androidx.transition.Visibility {
constructor(animatorSet: android.animation.AnimatorSet, transitionName: string); constructor(animatorSet: android.animation.AnimatorSet, transitionName: string);

View File

@ -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 + " }";
}
}

View File

@ -1,109 +1,33 @@
/**
*
*/
package org.nativescript.widgets; package org.nativescript.widgets;
/**
* @author triniwiz
*/
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable; import android.util.Log;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.json.JSONException;
import org.json.JSONObject;
public class Utils { public class Utils {
public static void drawBoxShadow(View view, String value) { public static void drawBoxShadow(View view, String value) {
try { if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.M) {
JSONObject config = new JSONObject(value); return;
int shadowColor = config.getInt("shadowColor"); }
int cornerRadius = config.getInt("cornerRadius"); Log.d("BoxShadowDrawable", "drawBoxShadow");
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");
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; int count = 0;
while (view.getParent() != null && view.getParent() instanceof ViewGroup) {
// 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++; count++;
ViewGroup parent = (ViewGroup) view.getParent(); ViewGroup parent = (ViewGroup) view.getParent();
parent.setClipChildren(false); parent.setClipChildren(false);
@ -112,11 +36,20 @@ public class Utils {
if (count == 1) { if (count == 1) {
break; break;
} }
nativeView = parent;
} }
}
nativeView.setBackground(drawable); public static void clearBoxShadow(View view) {
} catch (JSONException ignore) { 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);
} }
} }
} }