Currently you aren't able to set a gradient for a background with CSS. I'd also like to be able to use RGBA as a colour value to add transparency.
Implementation such as:
background: linear-gradient(left, rgba(255,0,0,0), rgba(255,0,0,1));
Would be great to support linear, radial and repeating backgrounds.
Here is an Android implementation that seems to work. The iOS code was ripped from somewhere - I don't know if it'll work.
var coreView = require("ui/core/view"),
colorModule = require("color"),
Color = colorModule.Color;
function linearGradient(root, viewId, colors, stops) {
var _colors = [],
_view = coreView.getViewById(root, viewId),
nativeView;
if (_view) {
nativeView = _view._nativeView;
} else {
throw TraceableException("Cannot find view '" + view + "' in page!");
}
if (!nativeView) {
return;
}
colors.forEach(function(c, idx) {
if (!(c instanceof Color)) {
colors[idx] = new Color(c);
}
});
if (this.android) {
var backgroundDrawable = nativeView.getBackground(),
orientation = android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM,
LINEAR_GRADIENT = 0;
// Get the android version of the colors
colors.forEach(function(c) {
_colors.push(c.android);
});
// If it isn't already gradient... make it so.
if (!(backgroundDrawable instanceof android.graphics.drawable.GradientDrawable)) {
backgroundDrawable = new android.graphics.drawable.GradientDrawable();
backgroundDrawable.setColors(_colors);
backgroundDrawable.setGradientType(LINEAR_GRADIENT);
nativeView.setBackgroundDrawable(backgroundDrawable);
}
} else if (this.ios) {
/*
UIView* view = [[UIView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, 320.0f, 50.0f)];
CAGradientLayer *gradient = [CAGradientLayer layer];
gradient.frame = view.bounds;
gradient.colors = [NSArray arrayWithObjects:(id)[[UIColor whiteColor] CGColor], (id)[[UIColor blackColor] CGColor], nil];
[view.layer insertSublayer:gradient atIndex:0];
*/
}
}
exports.linearGradient = linearGradient;
Then you use it like this:
// Set the background gradient
util.linearGradient(page, "contentArea", ['#FFFFFF', '#AAAAAA'], [0, 1]);
You'd put this in your "pageLoaded" event and pass args.object
as the first argument, the id
of the view from the XML, then the gradient colors (in hex format) as the third argument. The fourth argument isn't used and can just be [0,1]
as above. It was intended to be the color steps, but I never got to that. Oh yes, the XML needs to have the id
property defined:
<AbsoluteLayout id="contentArea" row="1" col="0" style="background-color: #0000ff;">
+1 for CSS gradient support. Would make it much easier to create even more polished NativeScript UI.
Even simple support for 2-step linear gradients would be a great start. Multiple steps, radial/repeating could come later. Also agree with @lscown that {N} should adopt the latest "new" CSS gradient syntax.
Reference: https://css-tricks.com/css3-gradients/
@bfattori - here is a version of your function with the iOS implementation. I also changed the if-statements, so checking the platform does not depend on this.
var platform = require("platform");
var coreView = require("ui/core/view");
var colorModule = require("color");
var Color = colorModule.Color;
function pageLoaded(args) {
var page = args.object;
linearGradient(page, "contentArea", ['#123456', '#345645']);
}
exports.pageLoaded = pageLoaded;
var coreView = require("ui/core/view"),
colorModule = require("color"),
Color = colorModule.Color;
function linearGradient(root, viewId, colors, stops) {
console.log(platform.device.os);
var _colors = [],
_view = coreView.getViewById(root, viewId),
nativeView;
if (_view) {
nativeView = _view._nativeView;
} else {
throw TraceableException("Cannot find view '" + view + "' in page!");
}
if (!nativeView) {
return;
}
colors.forEach(function(c, idx) {
if (!(c instanceof Color)) {
colors[idx] = new Color(c);
}
});
if (platform.device.os=== platform.platformNames.android) {
console.log("android");
var backgroundDrawable = nativeView.getBackground(),
orientation = android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM,
LINEAR_GRADIENT = 0;
colors.forEach(function(c) {
_colors.push(c.android);
});
if (!(backgroundDrawable instanceof android.graphics.drawable.GradientDrawable)) {
backgroundDrawable = new android.graphics.drawable.GradientDrawable();
backgroundDrawable.setColors(_colors);
backgroundDrawable.setGradientType(LINEAR_GRADIENT);
nativeView.setBackgroundDrawable(backgroundDrawable);
}
} else if (platform.device.os === platform.platformNames.ios) {
console.log("ios");
var view = root.ios.view;
var colorsArray = NSMutableArray.alloc().initWithCapacity(2);
colors.forEach(function(c) {
colorsArray.addObject(interop.types.id(c.ios.CGColor));
});
var gradientLayer = CAGradientLayer.layer();
gradientLayer.colors = colorsArray;
gradientLayer.frame = view.bounds;
view.layer.insertSublayerAtIndex(gradientLayer,0);
}
}
exports.linearGradient = linearGradient;
And also, don't forget to set the actual orientation to the GradientDrawable:
backgroundDrawable.setOrientation(orientation);
Thanks @bfattori and @N3ll for your solution. I just put all together into an Angular 2 directive, so it's really easy to use:
<StackLayout [gradient]="'#33827D'" [endColor]="'#27758C'"></StackLayout >
Code:
import {Directive, ElementRef, Input, OnInit} from "@angular/core";
import * as application from "application";
import {Color} from "color";
import {Page} from "ui/page";
declare var android: any;
declare var NSMutableArray: any;
declare var CAGradientLayer: any;
declare var interop: any;
@Directive({
selector: "[gradient]"
})
export class GradientDirective implements OnInit {
@Input('gradient')
start: string;
@Input('endColor')
end: string;
constructor(private el: ElementRef, private page: Page) {
}
ngOnInit(): void {
let startColor: Color = new Color(this.start);
let endColor: Color = new Color(this.end);
if (application.android) {
let backgroundDrawable = this.el.nativeElement.android.getBackground();
let orientation = android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM;
let LINEAR_GRADIENT: number = 0;
// If it isn't already gradient... make it so.
if (!(backgroundDrawable instanceof android.graphics.drawable.GradientDrawable)) {
backgroundDrawable = new android.graphics.drawable.GradientDrawable();
backgroundDrawable.setColors([startColor.android, endColor.android]);
backgroundDrawable.setGradientType(LINEAR_GRADIENT);
backgroundDrawable.setOrientation(orientation);
this.el.nativeElement.android.setBackgroundDrawable(backgroundDrawable);
}
} else {
let view = this.page.ios.view;
let colorsArray = NSMutableArray.alloc().initWithCapacity(2);
colorsArray.addObject(interop.types.id(startColor.ios.CGColor));
colorsArray.addObject(interop.types.id(endColor.ios.CGColor));
let gradientLayer = CAGradientLayer.layer();
gradientLayer.colors = colorsArray;
gradientLayer.frame = view.bounds;
view.layer.insertSublayerAtIndex(gradientLayer,0);
}
}
}
@N3ll @csell5 this currently seems to pick the entire page and apply the gradient (atleast on IOS, havent tested on android), is there a way to limit this to the element on which the [gradient]
directive has been applied
+1 for css implementation
@NgSculptor The problem with the iOS implementation of the Gradient Background, is that it relies on the dimensions of the view (through the 'bounds' property), which are not available during the OnInit hook. I have also tested with AfterViewInit instead, and it seems too early as well.
That's the reason why @Cselt 's snippet uses the page instead of the view itself.
As a (dirty) workaround, you could do something like this:
[...]
} else {
setTimeout(() => {
let view = this.el.nativeElement;
let colorsArray = NSMutableArray.alloc().initWithCapacity(2);
colorsArray.addObject(interop.types.id(startColor.ios.CGColor));
colorsArray.addObject(interop.types.id(endColor.ios.CGColor));
let gradientLayer = CAGradientLayer.layer();
gradientLayer.colors = colorsArray;
gradientLayer.frame = view.bounds;
view.ios.layer.insertSublayerAtIndex(gradientLayer,0);
}, 50);
}
[...]
This way, you're (almost) sure to catch the moment where the dimensions of the view are available. A cleaner solution would be to get notified when the dimensions of the view get available. Does anyone know if there is a way to do this?
@bdauria
It's a bit hacky, but I solved it by overriding the _onSizeChanged
function on this.el.nativeElement
.
import { Directive, ElementRef, Input, OnInit, OnDestroy, AfterViewInit } from '@angular/core';
import * as application from 'application';
import { Color } from 'color';
import { Page } from 'ui/page';
import { View } from 'ui/core/view';
declare var android: any;
declare var NSMutableArray: any;
declare var CAGradientLayer: any;
declare var interop: any;
@Directive({
selector: '[gradient]'
})
export class GradientDirective implements OnInit, OnDestroy, AfterViewInit {
@Input('gradient')
start: string;
@Input('endColor')
end: string;
private loadedEventFn: () => void;
constructor(private el: ElementRef, private page: Page) {
}
ngOnInit() {
const view = this.el.nativeElement;
const startColor: Color = new Color(this.start);
view.backgroundColor = startColor;
this.loadedEventFn = () => {
this.setGradient();
};
this.page.on(Page.loadedEvent, this.loadedEventFn);
try {
const oldOnSizeChangedFn = this.el.nativeElement._onSizeChanged;
if (this.el.nativeElement.ios) {
this.el.nativeElement._onSizeChanged = () => {
oldOnSizeChangedFn.call(this.el.nativeElement);
this.setGradient();
};
}
} catch (exp) {
console.log(exp);
}
}
ngAfterViewInit() {
this.setGradient();
}
ngOnDestroy() {
console.log(`GradientDirective.ngOnDestroy()`);
this.page.off(Page.loadedEvent, this.loadedEventFn);
}
private setGradient() {
console.log(`GradientDirective.setGradient()`);
const startColor: Color = new Color(this.start);
const endColor: Color = new Color(this.end);
const view = this.el.nativeElement;
if (view.android) {
let backgroundDrawable = view.android.getBackground();
const orientation = android.graphics.drawable.GradientDrawable.Orientation.TOP_BOTTOM;
const LINEAR_GRADIENT: number = 0;
// If it isn't already gradient... make it so.
if (!(backgroundDrawable instanceof android.graphics.drawable.GradientDrawable)) {
backgroundDrawable = new android.graphics.drawable.GradientDrawable();
backgroundDrawable.setColors([startColor.android, endColor.android]);
backgroundDrawable.setGradientType(LINEAR_GRADIENT);
backgroundDrawable.setOrientation(orientation);
this.el.nativeElement.android.setBackgroundDrawable(backgroundDrawable);
}
} else if (view.ios && view._nativeView && view._nativeView.bounds) {
const nativeView = view._nativeView;
const colorsArray = NSMutableArray.alloc().initWithCapacity(2);
colorsArray.addObject(interop.types.id(startColor.ios.CGColor));
colorsArray.addObject(interop.types.id(endColor.ios.CGColor));
const gradientLayer = CAGradientLayer.layer();
gradientLayer.colors = colorsArray;
gradientLayer.frame = nativeView.bounds;
nativeView.layer.insertSublayerAtIndex(gradientLayer, 0);
}
}
}
Note: I've made a few improvements over the original.
this.el.nativeElement._onSizeChanged
, this is private in ui/core/view
and only exists on iOS
.this.el.nativeElement
just incase the gradient is never applied.@cindy-m
Do you have a code example I could look at?
@m-abs
I can't provide the code, but I can try to explain what I try to do.
I use the gradient in the html of a component that is used as the header in one of my pages after routing to it.
Because of different objects I want to show there, I use a Stacklayout. Something like this:
with the following styling:
.header{
padding: 24
color:green
}
.header-body{
font-size: 14
}
.header-caption{
font-size: 9
}
When I add the gradient to the StackLayout, it fills the whole stack layout, but splits it in two. The first part of it is where the text would end without any padding.
I tried a lot to get the gradient to work right and the only way it works perfect is, when I use styling="height:90;" in the Stacklayout.
Including the height in the css does not seem to work.
(I removed the previous comment because I wanted to explain the problem I had better to you :) )
Seems also to break if used in the menu and routing to another page :(
(on the first page it looks fine, but as soo as it is moved to another page, the gradient seems to break)
Re: iOS does this only work on like the root page? I can't seem to get it to work on anything but page.ios.view
I just need to apply a gradient to a generic view (Grid or stack) on the page, trying in the loaded event, and passing it in args.object instead of using the above page.getViewById... doesn't crash, but also doesn't do anything.
<GridLayout id="contentView" loaded="onLoaded"
var coreView = require("ui/core/view"),
colorModule = require("color"),
Color = colorModule.Color;
function linearGradient(view, colors) {
debugger;
var _colors = [];
var _view = view;
var nativeView;
if (_view) {
nativeView = _view._nativeView;
} else {
throw TraceableException("Cannot find view '" + view + "' in page!");
}
if (!nativeView) {
return;
}
colors.forEach(function(c, idx) {
if (!(c instanceof Color)) {
colors[idx] = new Color(c);
}
});
if (platform.device.os=== platform.platformNames.android) {
//CUT
} else if (platform.device.os === platform.platformNames.ios) {
var view = _view.ios;
var colorsArray = NSMutableArray.alloc().initWithCapacity(2);
colors.forEach(function(c) {
colorsArray.addObject(interop.types.id(c.ios.CGColor));
});
var gradientLayer = CAGradientLayer.layer();
gradientLayer.colors = colorsArray;
gradientLayer.frame = view.bounds;
view.layer.insertSublayerAtIndex(gradientLayer,0);
}
}
exports.linearGradient = linearGradient;
While you're waiting for CSS gradients (or don't want to fiddle with native code yourself), you can now drop in nativescript-gradient for x-plat gradient goodness. No external libraries will be added to your project.
Just replace a StackLayout
by Gradient
, add a direction
and colors
.. and you have something like this screenshot:
With the state of 3.2 gradients can be pretty easily implemented.
Same (almost) for border gradients!
That's after the two PRs land:
Up for grabs anyone?
I found a way to end this issue. Don't know the drawbacks but it works everywhere. Even in buttons!
1) Get a gradient image.
2) Drag it to images folder or where ever you want it to be.
3) Put this code in .css files. For example under pages or buttons.
background-image: ~/images/bggrad.png;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
Any update on the priority for getting CSS Gradients in to {N} CSS? This would be a big boost to making it easier to create "better looking" {N} apps.
background-size
property not working with linear gradient
. any workaround for this?
In my app, at first module when app started (android) css background-color could load linear-gradient. However when I navigate to new module (lazy loading) CLI tell me that linear-gradient invalid. Any workaround for this?
p/s: I tried with background-image, it work correctly, but button had lost it's own behaviour when press and hold.
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
Most helpful comment
While you're waiting for CSS gradients (or don't want to fiddle with native code yourself), you can now drop in nativescript-gradient for x-plat gradient goodness. No external libraries will be added to your project.
Just replace a
StackLayout
byGradient
, add adirection
andcolors
.. and you have something like this screenshot: