mirror of
https://github.com/NativeScript/NativeScript.git
synced 2025-11-05 13:26:48 +08:00
Css gradients (#5534)
* feat(ios): Added support for css gradients. * feat(android): Added support for css gradients. * fix: Fixed gradient borders on ios * fix(gradient): added backgroundGradient to View and Style. * fix(ios-gradients): fixed ios gradients covering view content. * test(gradient): Added ui app tests for background gradients. * test(gradient): Added a test ensuring background gradient property is applied to style. * style(gradient): Fixed tslint errors. * fix(gradient): Removed the background-gradient property and added the gradient to background-image. * style: fixed a consecutive blank line tslint error. * fix(tests): fixed the bug that was causing tests to fail. * chore(linear-gradient): fix equality comparer * test(gradient): add linear gradients test app * chore(tslint): update with latest tslint rules
This commit is contained in:
@@ -21,9 +21,11 @@ import {
|
||||
|
||||
import { createViewFromEntry } from "../../builder";
|
||||
import { StyleScope } from "../../styling/style-scope";
|
||||
import { LinearGradient } from "../../styling/linear-gradient";
|
||||
|
||||
export * from "../../styling/style-properties";
|
||||
export * from "../view-base";
|
||||
export { LinearGradient };
|
||||
|
||||
import * as am from "../../animation";
|
||||
let animationModule: typeof am;
|
||||
@@ -435,10 +437,10 @@ export abstract class ViewCommon extends ViewBase implements ViewDefinition {
|
||||
this.style.backgroundColor = value;
|
||||
}
|
||||
|
||||
get backgroundImage(): string {
|
||||
get backgroundImage(): string | LinearGradient {
|
||||
return this.style.backgroundImage;
|
||||
}
|
||||
set backgroundImage(value: string) {
|
||||
set backgroundImage(value: string | LinearGradient) {
|
||||
this.style.backgroundImage = value;
|
||||
}
|
||||
|
||||
|
||||
4
tns-core-modules/ui/core/view/view.d.ts
vendored
4
tns-core-modules/ui/core/view/view.d.ts
vendored
@@ -7,9 +7,11 @@ import { ViewBase, Property, EventData, Color } from "../view-base";
|
||||
import { Animation, AnimationDefinition, AnimationPromise } from "../../animation";
|
||||
import { HorizontalAlignment, VerticalAlignment, Visibility, Length, PercentLength } from "../../styling/style-properties";
|
||||
import { GestureTypes, GestureEventData, GesturesObserver } from "../../gestures";
|
||||
import { LinearGradient } from "../../styling/linear-gradient";
|
||||
|
||||
export * from "../view-base";
|
||||
export * from "../../styling/style-properties";
|
||||
export { LinearGradient };
|
||||
|
||||
export function PseudoClassHandler(...pseudoClasses: string[]): MethodDecorator;
|
||||
|
||||
@@ -219,7 +221,7 @@ export abstract class View extends ViewBase {
|
||||
/**
|
||||
* Gets or sets the background image of the view.
|
||||
*/
|
||||
backgroundImage: string;
|
||||
backgroundImage: string | LinearGradient;
|
||||
|
||||
/**
|
||||
* Gets or sets the minimum width the view may grow to.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Deifinitions.
|
||||
import { Background as BackgroundDefinition } from "./background";
|
||||
import { BackgroundRepeat } from "../core/view";
|
||||
import { BackgroundRepeat, LinearGradient } from "../core/view";
|
||||
|
||||
// Types.
|
||||
import { Color } from "../../color";
|
||||
@@ -9,7 +9,7 @@ export class Background implements BackgroundDefinition {
|
||||
public static default = new Background();
|
||||
|
||||
public color: Color;
|
||||
public image: string;
|
||||
public image: string | LinearGradient;
|
||||
public repeat: BackgroundRepeat;
|
||||
public position: string;
|
||||
public size: string;
|
||||
@@ -58,7 +58,7 @@ export class Background implements BackgroundDefinition {
|
||||
return clone;
|
||||
}
|
||||
|
||||
public withImage(value: string): Background {
|
||||
public withImage(value: string | LinearGradient): Background {
|
||||
const clone = this.clone();
|
||||
clone.image = value;
|
||||
return clone;
|
||||
@@ -179,8 +179,15 @@ export class Background implements BackgroundDefinition {
|
||||
return false;
|
||||
}
|
||||
|
||||
let imagesEqual = false;
|
||||
if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) {
|
||||
imagesEqual = LinearGradient.equals(value1, value2);
|
||||
} else {
|
||||
imagesEqual = value1.image === value2.image;
|
||||
}
|
||||
|
||||
return Color.equals(value1.color, value2.color)
|
||||
&& value1.image === value2.image
|
||||
&& imagesEqual
|
||||
&& value1.position === value2.position
|
||||
&& value1.repeat === value2.repeat
|
||||
&& value1.size === value2.size
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { View } from "../core/view";
|
||||
import { View, LinearGradient } from "../core/view";
|
||||
import { isDataURI, isFileOrResourcePath, layout, RESOURCE_PREFIX, FILE_PREFIX } from "../../utils/utils";
|
||||
import { parse } from "../../css-value";
|
||||
import { path, knownFolders } from "../../file-system";
|
||||
@@ -104,6 +104,38 @@ function fromBase64(source: string): android.graphics.Bitmap {
|
||||
return android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.length)
|
||||
}
|
||||
|
||||
function fromGradient(gradient: LinearGradient): org.nativescript.widgets.LinearGradientDefinition {
|
||||
const colors = Array.create("int", gradient.colorStops.length);
|
||||
const stops = Array.create("float", gradient.colorStops.length);
|
||||
let hasStops = false;
|
||||
gradient.colorStops.forEach((stop, index) => {
|
||||
colors[index] = stop.color.android;
|
||||
if (stop.offset) {
|
||||
stops[index] = stop.offset.value;
|
||||
hasStops = true;
|
||||
}
|
||||
});
|
||||
|
||||
const alpha = gradient.angle / (Math.PI * 2);
|
||||
const startX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.75)),
|
||||
2
|
||||
);
|
||||
const startY = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.5)),
|
||||
2
|
||||
);
|
||||
const endX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.25)),
|
||||
2
|
||||
);
|
||||
const endY = Math.pow(
|
||||
Math.sin(Math.PI * alpha),
|
||||
2
|
||||
);
|
||||
return new org.nativescript.widgets.LinearGradientDefinition(startX, startY, endX, endY, colors, hasStops ? stops : null);
|
||||
}
|
||||
|
||||
const pattern: RegExp = /url\(('|")(.*?)\1\)/;
|
||||
function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativescript.widgets.BorderDrawable) {
|
||||
const nativeView = <android.view.View>view.nativeViewProtected;
|
||||
@@ -115,8 +147,9 @@ function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativ
|
||||
const backgroundSizeParsedCSSValues = createNativeCSSValueArray(background.size);
|
||||
const blackColor = -16777216; //android.graphics.Color.BLACK;
|
||||
|
||||
let imageUri = background.image;
|
||||
if (imageUri) {
|
||||
let imageUri: string;
|
||||
if (background.image && typeof background.image === "string") {
|
||||
imageUri = background.image;
|
||||
const match = imageUri.match(pattern);
|
||||
if (match && match[2]) {
|
||||
imageUri = match[2];
|
||||
@@ -141,6 +174,11 @@ function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativ
|
||||
}
|
||||
}
|
||||
|
||||
let gradient: org.nativescript.widgets.LinearGradientDefinition = null;
|
||||
if (background.image && background.image instanceof LinearGradient) {
|
||||
gradient = fromGradient(background.image);
|
||||
}
|
||||
|
||||
borderDrawable.refresh(
|
||||
background.borderTopColor ? background.borderTopColor.android : blackColor,
|
||||
background.borderRightColor ? background.borderRightColor.android : blackColor,
|
||||
@@ -162,6 +200,7 @@ function refreshBorderDrawable(this: void, view: View, borderDrawable: org.nativ
|
||||
background.color ? background.color.android : 0,
|
||||
imageUri,
|
||||
bitmap,
|
||||
gradient,
|
||||
context,
|
||||
background.repeat,
|
||||
background.position,
|
||||
|
||||
6
tns-core-modules/ui/styling/background.d.ts
vendored
6
tns-core-modules/ui/styling/background.d.ts
vendored
@@ -3,12 +3,12 @@
|
||||
*/ /** */
|
||||
|
||||
import { Color } from "../../color";
|
||||
import { View, BackgroundRepeat } from "../core/view";
|
||||
import { View, BackgroundRepeat, LinearGradient } from "../core/view";
|
||||
|
||||
export class Background {
|
||||
public static default: Background;
|
||||
public color: Color;
|
||||
public image: string;
|
||||
public image: string | LinearGradient;
|
||||
public repeat: BackgroundRepeat;
|
||||
public position: string;
|
||||
public size: string;
|
||||
@@ -27,7 +27,7 @@ export class Background {
|
||||
public clipPath: string;
|
||||
|
||||
public withColor(value: Color): Background;
|
||||
public withImage(value: string): Background;
|
||||
public withImage(value: string | LinearGradient): Background;
|
||||
public withRepeat(value: BackgroundRepeat): Background;
|
||||
public withPosition(value: string): Background;
|
||||
public withSize(value: string): Background;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ScrollEventData } from "../scroll-view";
|
||||
|
||||
import { Background as BackgroundDefinition } from "./background";
|
||||
import { View, Point } from "../core/view";
|
||||
import { View, Point, LinearGradient } from "../core/view";
|
||||
import { Color } from "../../color";
|
||||
import { ios as utilsIos, isDataURI, isFileOrResourcePath, layout } from "../../utils/utils";
|
||||
import { fromFileOrResource, fromBase64, fromUrl } from "../../image-source";
|
||||
@@ -21,6 +21,8 @@ interface NativeView extends UIView {
|
||||
rightBorderLayer: CALayer;
|
||||
bottomBorderLayer: CALayer;
|
||||
leftBorderLayer: CALayer;
|
||||
|
||||
gradientLayer: CAGradientLayer;
|
||||
}
|
||||
|
||||
interface Rect {
|
||||
@@ -43,6 +45,11 @@ export module ios {
|
||||
clearNonUniformBorders(nativeView);
|
||||
}
|
||||
|
||||
clearGradient(nativeView);
|
||||
if (background.image instanceof LinearGradient) {
|
||||
drawGradient(nativeView, background.image);
|
||||
}
|
||||
|
||||
const hasNonUniformBorderWidths = background.hasBorderWidth() && !background.hasUniformBorder();
|
||||
const hasNonUniformBorderRadiuses = background.hasBorderRadius() && !background.hasUniformBorderRadius();
|
||||
if (background.hasUniformBorderColor() && (hasNonUniformBorderWidths || hasNonUniformBorderRadiuses)) {
|
||||
@@ -67,7 +74,7 @@ export module ios {
|
||||
drawClipPath(nativeView, background);
|
||||
}
|
||||
|
||||
if (!background.image) {
|
||||
if (!background.image || background.image instanceof LinearGradient) {
|
||||
const uiColor = background.color ? background.color.ios : undefined;
|
||||
callback(uiColor);
|
||||
} else {
|
||||
@@ -151,7 +158,7 @@ function setUIColorFromImage(view: View, nativeView: UIView, callback: (uiColor:
|
||||
|
||||
const style = view.style;
|
||||
const background = style.backgroundInternal;
|
||||
let imageUri = background.image;
|
||||
let imageUri = background.image as string;
|
||||
if (imageUri) {
|
||||
const match = imageUri.match(pattern);
|
||||
if (match && match[2]) {
|
||||
@@ -663,6 +670,60 @@ function drawNoRadiusNonUniformBorders(nativeView: NativeView, background: Backg
|
||||
nativeView.hasNonUniformBorder = hasNonUniformBorder;
|
||||
}
|
||||
|
||||
function drawGradient(nativeView: NativeView, gradient: LinearGradient) {
|
||||
|
||||
const gradientLayer = CAGradientLayer.layer();
|
||||
gradientLayer.frame = nativeView.bounds;
|
||||
nativeView.gradientLayer = gradientLayer;
|
||||
|
||||
const iosColors = NSMutableArray.alloc().initWithCapacity(gradient.colorStops.length);
|
||||
const iosStops = NSMutableArray.alloc<number>().initWithCapacity(gradient.colorStops.length);
|
||||
let hasStops = false;
|
||||
|
||||
gradient.colorStops.forEach(stop => {
|
||||
iosColors.addObject(stop.color.ios.CGColor);
|
||||
if (stop.offset) {
|
||||
iosStops.addObject(stop.offset.value);
|
||||
hasStops = true;
|
||||
}
|
||||
});
|
||||
|
||||
gradientLayer.colors = iosColors;
|
||||
|
||||
if (hasStops) {
|
||||
gradientLayer.locations = iosStops;
|
||||
}
|
||||
|
||||
const alpha = gradient.angle / (Math.PI * 2);
|
||||
const startX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.75)),
|
||||
2
|
||||
);
|
||||
const startY = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.5)),
|
||||
2
|
||||
);
|
||||
const endX = Math.pow(
|
||||
Math.sin(Math.PI * (alpha + 0.25)),
|
||||
2
|
||||
);
|
||||
const endY = Math.pow(
|
||||
Math.sin(Math.PI * alpha),
|
||||
2
|
||||
);
|
||||
gradientLayer.startPoint = {x: startX, y: startY};
|
||||
gradientLayer.endPoint = {x: endX, y: endY};
|
||||
|
||||
nativeView.layer.insertSublayerAtIndex(gradientLayer, 0);
|
||||
|
||||
}
|
||||
|
||||
function clearGradient(nativeView: NativeView): void {
|
||||
if (nativeView.gradientLayer) {
|
||||
nativeView.gradientLayer.removeFromSuperlayer();
|
||||
}
|
||||
}
|
||||
|
||||
function drawClipPath(nativeView: UIView, background: BackgroundDefinition) {
|
||||
const layer = nativeView.layer;
|
||||
const layerBounds = layer.bounds;
|
||||
|
||||
64
tns-core-modules/ui/styling/linear-gradient.ts
Normal file
64
tns-core-modules/ui/styling/linear-gradient.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { LengthPercentUnit } from "./style-properties";
|
||||
import * as parser from "../../css/parser";
|
||||
import { Color } from "../../color";
|
||||
|
||||
export class LinearGradient {
|
||||
public angle: number;
|
||||
public colorStops: ColorStop[];
|
||||
|
||||
static parse(value: parser.LinearGradient): LinearGradient {
|
||||
const result = new LinearGradient();
|
||||
result.angle = value.angle;
|
||||
result.colorStops = value.colors.map(color => {
|
||||
const offset = color.offset || null;
|
||||
let offsetUnit: LengthPercentUnit;
|
||||
|
||||
if (offset && offset.unit === "%") {
|
||||
offsetUnit = {
|
||||
unit: "%",
|
||||
value: offset.value
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
color: new Color(color.argb),
|
||||
offset: offsetUnit
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
static equals(first: LinearGradient, second: LinearGradient): boolean {
|
||||
if (!first && !second) {
|
||||
return true;
|
||||
} else if (!first || !second) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (first.angle !== second.angle) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (first.colorStops.length !== second.colorStops.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < first.colorStops.length; i++) {
|
||||
const firstStop = first.colorStops[i];
|
||||
const secondStop = second.colorStops[i];
|
||||
if (firstStop.offset !== secondStop.offset) {
|
||||
return false;
|
||||
}
|
||||
if (!Color.equals(firstStop.color, secondStop.color)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ColorStop {
|
||||
color: Color;
|
||||
offset?: LengthPercentUnit;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// Types
|
||||
import { unsetValue, Style,
|
||||
CssProperty, CssAnimationProperty,
|
||||
import { unsetValue, Style,
|
||||
CssProperty, CssAnimationProperty,
|
||||
ShorthandProperty, InheritedCssProperty,
|
||||
makeValidator, makeParser } from "../core/properties";
|
||||
import {
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
} from "../../matrix";
|
||||
|
||||
import * as parser from "../../css/parser";
|
||||
import { LinearGradient } from "./linear-gradient";
|
||||
|
||||
export type LengthDipUnit = { readonly unit: "dip", readonly value: dip };
|
||||
export type LengthPxUnit = { readonly unit: "px", readonly value: px };
|
||||
@@ -563,10 +564,28 @@ export const backgroundInternalProperty = new CssProperty<Style, Background>({
|
||||
backgroundInternalProperty.register(Style);
|
||||
|
||||
// const pattern: RegExp = /url\(('|")(.*?)\1\)/;
|
||||
export const backgroundImageProperty = new CssProperty<Style, string>({
|
||||
export const backgroundImageProperty = new CssProperty<Style, string | LinearGradient>({
|
||||
name: "backgroundImage", cssName: "background-image", valueChanged: (target, oldValue, newValue) => {
|
||||
const background = target.backgroundInternal.withImage(newValue);
|
||||
target.backgroundInternal = background;
|
||||
},
|
||||
equalityComparer: (value1, value2) => {
|
||||
if (value1 instanceof LinearGradient && value2 instanceof LinearGradient) {
|
||||
return LinearGradient.equals(value1, value2)
|
||||
} else {
|
||||
return value1 === value2;
|
||||
}
|
||||
},
|
||||
valueConverter: (value: any) => {
|
||||
if (typeof value === "string") {
|
||||
const parsed = parser.parseBackground(value);
|
||||
if (parsed) {
|
||||
const background = parsed.value;
|
||||
value = (typeof background.image === "object") ? LinearGradient.parse(background.image) : value;
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
});
|
||||
backgroundImageProperty.register(Style);
|
||||
@@ -618,7 +637,14 @@ function convertToBackgrounds(this: void, value: string): [CssProperty<any, any>
|
||||
if (typeof value === "string") {
|
||||
const backgrounds = parser.parseBackground(value).value;
|
||||
const backgroundColor = backgrounds.color ? new Color(backgrounds.color) : unsetValue;
|
||||
const backgroundImage = backgrounds.image || unsetValue;
|
||||
|
||||
let backgroundImage: string | LinearGradient;
|
||||
if (typeof backgrounds.image === "object" && backgrounds.image) {
|
||||
backgroundImage = LinearGradient.parse(backgrounds.image);
|
||||
} else {
|
||||
backgroundImage = backgrounds.image || unsetValue;
|
||||
}
|
||||
|
||||
const backgroundRepeat = backgrounds.repeat || unsetValue;
|
||||
const backgroundPosition = backgrounds.position ? backgrounds.position.text : unsetValue;
|
||||
|
||||
|
||||
3
tns-core-modules/ui/styling/style/style.d.ts
vendored
3
tns-core-modules/ui/styling/style/style.d.ts
vendored
@@ -11,6 +11,7 @@ import {
|
||||
FlexDirection, FlexWrap, JustifyContent, AlignItems, AlignContent,
|
||||
Order, FlexGrow, FlexShrink, FlexWrapBefore, AlignSelf
|
||||
} from "../../layouts/flexbox-layout";
|
||||
import { LinearGradient } from "../linear-gradient";
|
||||
|
||||
export interface Thickness {
|
||||
left: number;
|
||||
@@ -64,7 +65,7 @@ export class Style extends Observable {
|
||||
|
||||
public background: string;
|
||||
public backgroundColor: Color;
|
||||
public backgroundImage: string;
|
||||
public backgroundImage: string | LinearGradient;
|
||||
public backgroundRepeat: BackgroundRepeat;
|
||||
public backgroundSize: string;
|
||||
public backgroundPosition: string;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Style as StyleDefinition } from ".";
|
||||
import { Color } from "../../../color";
|
||||
import { Font, FontStyle, FontWeight } from "../font";
|
||||
import { Background } from "../background";
|
||||
import { Length, PercentLength, ViewBase, BackgroundRepeat, Visibility, HorizontalAlignment, VerticalAlignment, dip } from "../../core/view";
|
||||
import { Length, PercentLength, ViewBase, BackgroundRepeat, Visibility,
|
||||
HorizontalAlignment, VerticalAlignment, dip, LinearGradient } from "../../core/view";
|
||||
import { Observable } from "../../../data/observable";
|
||||
|
||||
import {
|
||||
@@ -37,7 +38,7 @@ export class Style extends Observable implements StyleDefinition {
|
||||
|
||||
public background: string;
|
||||
public backgroundColor: Color;
|
||||
public backgroundImage: string;
|
||||
public backgroundImage: string | LinearGradient;
|
||||
public backgroundRepeat: BackgroundRepeat;
|
||||
public backgroundSize: string;
|
||||
public backgroundPosition: string;
|
||||
|
||||
Reference in New Issue
Block a user