Logo

dev-resources.site

for different kinds of informations.

Rendering NativeScript Angular Templates and Components into images

Published at
1/10/2023
Categories
nativescript
viewshot
screenshot
mobile
Author
edusperoni
Author
10 person written this
edusperoni
open
Rendering NativeScript Angular Templates and Components into images

While working on a NativeScript Angular app with millions of downloads across platforms, I faced a tricky problem: I needed to generate an image that the user could share. Usually this can be done quite easily if you this view is visible in your application, where you could just render it to an image (in fact, it has been done before https://www.npmjs.com/package/nativescript-cscreenshot). The difficult part here was that this view did not show anywhere in the app, and even had special layout constraints.

Taking a view screenshot

Taking a screenshot of a view is an easy task.

On android, it’s a simple case of create a bitmap, attaching it to a canvas, and then drawing the view directly on that canvas:

export function renderToImageSource(hostView: View): ImageSource {
 const bitmap = android.graphics.Bitmap.createBitmap(hostView.android.getWidth(), hostView.android.getHeight(), android.graphics.Bitmap.Config.ARGB_8888);
 const canvas = new android.graphics.Canvas(bitmap);
 // ensure we start with a blank transparent canvas
 canvas.drawARGB(0, 0, 0, 0);
 hostView.android.draw(canvas);
 return new ImageSource(bitmap);
}
Enter fullscreen mode Exit fullscreen mode

On the iOS side, we have a very similar concept. We begin the image context, and then we render the view in that context:

export function renderToImageSource(hostView: View): ImageSource {
 UIGraphicsBeginImageContextWithOptions(CGSizeMake(hostView.ios.frame.size.width, hostView.ios.frame.size.height), false, Screen.mainScreen.scale);
 (hostView.ios as UIView).layer.renderInContext(UIGraphicsGetCurrentContext());
 const image = UIGraphicsGetImageFromCurrentImageContext();
 UIGraphicsEndImageContext();
 return new ImageSource(image);
}
Enter fullscreen mode Exit fullscreen mode

There it is! Taking a screenshot of any NativeScript view with a couple of lines of code!

View screenshotted multiple times, generating a Droste effect

Rendering a view detached from the view hierarchy

Now let’s take it one step further. Let’s use some clever NativeScript magic and create our layout completely detached from the native view tree:

export function loadViewInBackground(view: View): void {
 // get the context (android only)
 const context = isAndroid ? Utils.android.getApplicationContext() : {};
 // now create the native view and setup the styles (CSS) as if it were a root view
 view._setupAsRootView(context);
 // load the view to apply all the native properties
 view.callLoaded();
}
Enter fullscreen mode Exit fullscreen mode

That should do it! Now let’s just call that function and… oh…

Image description

Of course! This view has no size! So we need to measure and layout it. That’s simple enough:

export function measureAndLayout(hostView: View, width?: number, height?: number) {
 const dpWidth = width ? Utils.layout.toDevicePixels(width) : 0;
 const dpHeight = height ? Utils.layout.toDevicePixels(height) : 0;
 const infinity = Utils.layout.makeMeasureSpec(0, Utils.layout.UNSPECIFIED);
 hostView.measure(width ? Utils.layout.makeMeasureSpec(dpWidth, Utils.layout.EXACTLY) : infinity, height ? Utils.layout.makeMeasureSpec(dpHeight, Utils.layout.EXACTLY) : infinity);

 hostView.layout(0, 0, hostView.getMeasuredWidth(), hostView.getMeasuredHeight());
}
Enter fullscreen mode Exit fullscreen mode

Now this view should render exactly at the width and height that I require. Let’s give it a try:

A label being rendered into an image

It worked! Turns out it wasn’t as difficult as I thought. Now that we’re ready to go, let’s add the styling. Let’s keep the text intact, but add some styling. We need some border-radius and some margins.

.view-shot {
  border-radius: 50%;
  border-width: 1;
  border-color: red;
  margin: 10;
}
Enter fullscreen mode Exit fullscreen mode

Now run that through our render and…

Generated view has correct styling but no margins

Where did my margins go? Well, it turns out that, on both platforms, the parent layout is responsible for the children's positioning, and margins are just some extra positioning information given to the parent. Another quick fix then, just wrap the view with another layout:

export function loadViewInBackground(view: View): View {
 // get the context (android only)
 const context = isAndroid ? Utils.android.getApplicationContext() : {};
 // create a host view to ensure we're preserving margins
 const hostView = new GridLayout();
 hostView.addChild(view);
 // now create the native view and setup the styles (CSS) as if it were a root view
 hostView._setupAsRootView(context);
 // load the view to apply all the native properties
 hostView.callLoaded();
 return hostView;
}
Enter fullscreen mode Exit fullscreen mode

And the result:

Image finally generated with correct styling and margins

Success! We can now keep adding the remainder, like an image. The image has to be downloaded, so let’s add a delay between creating the view and screenshotting it (we can cache it later). And… oh no, not again.

iOS image working, android only shows label

Attaching the view to the view hierarchy

After digging through the native source code I realized that on Android a lot of views (like image) will only fully render when they’re attached to the window, so how do we attach it to the view hierarchy without showing it and without affecting the layout at all?

The main function of a ViewGroup is to layout the views in a particular way. So first, let’s create a view that will not do any layout:

@NativeClass
class DummyViewGroup extends android.view.ViewGroup {
 constructor(context: android.content.Context) {
   super(context);
   return global.__native(this);
 }
 public onMeasure(): void {
   this.setMeasuredDimension(0, 0);
 }
 public onLayout(): void {
   //
 }
}
class ContentViewDummy extends ContentView {
 createNativeView() {
   return new DummyViewGroup(this._context);
 }
}
Enter fullscreen mode Exit fullscreen mode

Now we just need to make sure that it’s visibility is set to collapse and use a very convenient method from the AppCompatActivity (addContentView) to add the view to the root of the activity, essentially adding it to the window but completely invisible.

export function loadViewInBackground(view: View) {
 const hiddenHost = new ContentViewDummy();
 const hostView = new GridLayout(); // use a host view to ensure margins are respected
 hiddenHost.content = hostView;
 hiddenHost.visibility = 'collapse';
 hostView.addChild(view);
 hiddenHost._setupAsRootView(Utils.android.getApplicationContext());
 hiddenHost.callLoaded();

 Application.android.startActivity.addContentView(hiddenHost.android, new android.view.ViewGroup.LayoutParams(0, 0));

 return {
   hiddenHost,
   hostView
 };
}
Enter fullscreen mode Exit fullscreen mode

Layout with label, image and CSS displaying correctly on both platforms

And we’re done!

Integrating with Angular

So far we have only dealt with NativeScript views, but what we really care is how we generate these views from Angular components and templates. So here's how:

import { ComponentRef, inject, Injectable, Injector, TemplateRef, Type, ViewContainerRef } from '@angular/core';

import { generateNativeScriptView, isDetachedElement, isInvisibleNode, NgView, NgViewRef } from '@nativescript/angular';
import { ContentView, ImageSource, View, ViewBase } from '@nativescript/core';
import { disposeBackgroundView, loadViewInBackground, measureAndLayout, renderToImageSource } from '@valor/nativescript-view-shot';

export interface DrawableOptions<T = unknown> {
  /**
   * target width of the view and image, in dip. If not specified, the measured width of the view will be used.
   */
  width?: number;
  /**
   * target height of the view and image, in dip. If not specified, the measured height of the view will be used.
   */
  height?: number;
  /**
   * how much should we delay the rendering of the view into the image.
   * This is useful if you want to wait for an image to load before rendering the view.
   * If using a function, it will be called with the NgViewRef as the first argument.
   * The NgViewRef can be used to get the EmbeddedViewRef/ComponentRef and the NativeScript views.
   * This is useful as you can fire an event in your views when the view is ready, and then complete
   * the promise to finish rendering to image.
   */
  delay?: number | ((viewRef: NgViewRef<T>) => Promise<void>);
  /**
   * The logical host of the view. This is used to specify where in the DOM this view should lie.
   * The practical use of this is if you want the view to inherit CSS styles from a parent.
   * If this is not specified, the view will be handled as a root view,
   * meaning no ancestor styles will be applied, similar to dropping the view in app.component.html
   */
  logicalHost?: ViewBase | ViewContainerRef;
}

@Injectable({
  providedIn: 'root',
})
export class ViewShotService {
  private myInjector = inject(Injector);
  async captureInBackground<T>(type: Type<T> | TemplateRef<T>, { width, height, delay, logicalHost }: DrawableOptions<T> = {}): Promise<ImageSource> {
    // use @nativescript/angular helper to create a view
    const ngView = generateNativeScriptView(type, {
      injector: logicalHost instanceof ViewContainerRef ? logicalHost.injector : this.myInjector),
      keepNativeViewAttached: true,
    });
    // detect changes on the component
    if (ngView.ref instanceof ComponentRef) {
      ngView.ref.changeDetectorRef.detectChanges();
    } else {
      ngView.ref.detectChanges();
    }
    // currently generateNativeScriptView will generate the view wrapped in a ContentView
    // this is a minor bug that should be fixed in a future version on @nativescript/angular
    // so let's add a failsafe here to remove the parent if it exists
    if (ngView.view.parent) {
      if (ngView.view.parent instanceof ContentView) {
        ngView.view.parent.content = null;
      } else {
        ngView.view.parent._removeView(ngView.view);
      }
    }
    // use the method that loads a view in the background
    const drawableViews = loadViewInBackground(ngView.view, host);
    const { hostView } = drawableViews;

    // do the measuring of the hostView
    measureAndLayout(hostView, width, height);

    // this delay is either a function or time in ms
    // which is useful for letting async views load or animate
    if (typeof delay === 'function' || (typeof delay === 'number' && delay >= 0)) {
      if (typeof delay === 'number') {
        await new Promise<void>((resolve) =>
          setTimeout(() => {
            resolve();
          }, delay)
        );
      } else {
        await delay(ngView);
        if (ngView.ref instanceof ComponentRef) {
          ngView.ref.changeDetectorRef.detectChanges();
        } else {
          ngView.ref.detectChanges();
        }
      }
      // do a final measure after the last changes
      measureAndLayout(hostView, width, height);
    }

    // call the render function
    const result = renderToImageSource(hostView);

    // dispose views and component
    disposeBackgroundView(drawableViews);
    ngView.ref.destroy();
    return result;
  }

  // unchanged from the original implementation
  captureRenderedView(view: View) {
    return renderToImageSource(view);
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Hopefully this gave you an insight on how the native platforms display their views and how NativeScript can be used in advanced view hierarchy composition.

The NativeScript plugin has been released as @valor/nativescript-view-shot and you can check its source code in our shared plugin workspace.

You can now enjoy creating views in the background for either showing, saving or sharing them in social media, like the following mockup:

Mockup of a quiz result that can be shared in image form

About Valor Software:

Official NativeScript professional support partner, Valor is actively contributing to the NativeScript ecosystem, providing enterprise support, consulting, and team augmentation. Valor Software additionally helps with all aspects of the SDLC, web, backend and mobile. Book a call today or email us at [email protected] if you need help.

screenshot Article's
30 articles in total
Favicon
From 80 to 8000/m: A Journey of SEO Optimization (Part 1)
Favicon
CodeSnap : prendre des captures d'écran de code dans VS Code
Favicon
rails system test, save failed screenshots
Favicon
🚀 📸 Creating Accessible and Stunning code screenshots
Favicon
AVIF Studio - Web page screen capture Chrome extension Made with Svelte and WebAssembly.
Favicon
Edge: Screenshots einer Seite erstellen ohne Addons
Favicon
Web Scraping Using Image Processing
Favicon
Edge: Create screenshots of a page without addons
Favicon
How to Perform Screenshot Comparison in Playwright
Favicon
How to take screenshots effectively on windows 11
Favicon
How to screenshot webpages in Golang
Favicon
Screenshot all your pages
Favicon
CodeSnap: Take Code Screenshots In VS Code
Favicon
Rendering NativeScript Angular Templates and Components into images
Favicon
How to run a code in editor in Atom IDE
Favicon
Take a Full-Page Screenshot in Browser (without extension or add-on)
Favicon
Tomar capturas de pantalla facilmente en i3wm
Favicon
Building A Serverless Screenshot Service with Lambda
Favicon
Android - How to do Screenshot Testing in Jetpack Compose
Favicon
How to capture a screenshot of the single window you want (using command line).
Favicon
How to Scan QR Code from Desktop Screen in Python
Favicon
react-native detect when user takes a screenshot
Favicon
Set Flameshot as default screenshot app on Ubuntu :)
Favicon
How to capture Screenshot within the browser
Favicon
Capture Website Screenshots with Python
Favicon
Reducing a Screenshot Size in Mac
Favicon
Employee Monitoring Tool to improve employee productivity
Favicon
Configuring screenshots in Mac
Favicon
How to take a screenshot of Jira kanban
Favicon
How To Capture Screenshots In Selenium? Guide With Examples

Featured ones: