Nativescript: More support for CSS gradients

Created on 3 Aug 2015  Â·  20Comments  Â·  Source: NativeScript/NativeScript

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.

css help wanted low ♥ community PR

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 by Gradient, add a direction and colors.. and you have something like this screenshot:

tweet-gradient

All 20 comments

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.

  • loadedEvent on Page, to reapply the gradient on android if you changes to another app and back again.
  • Override this.el.nativeElement._onSizeChanged, this is private in ui/core/view and only exists on iOS.
  • Moved the gradient code to it's own function.
  • Set the startColor as background color on the 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:

tweet-gradient

With the state of 3.2 gradients can be pretty easily implemented.

  • [ ] CSS properties have to be created for background that would parse the CSS values.
  • [ ] The CSS property have to update the backgroundInternal property.
  • [ ] In Android the parsed gradient should propagate to our BorderDrawable, from the values a LinearGradient or RadialGradient should be created for background paint.
  • [ ] In iOS we create custom layers in some cases for background. Using gradient should use one of these cases and instead of the CAShapeLayer here a CAGradientLayer should be used, with the currently created path instead of geometry for the CAShapeLayer used as mask for the CAGradientLayer.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

guillaume-roy picture guillaume-roy  Â·  3Comments

Pourya8366 picture Pourya8366  Â·  3Comments

yclau picture yclau  Â·  3Comments

dhanalakshmitawwa picture dhanalakshmitawwa  Â·  3Comments

pocesar picture pocesar  Â·  3Comments