NativeScript / canvas

Apache License 2.0
88 stars 18 forks source link

Tab navigation breaks canvas on Android #41

Closed chris-praxis closed 2 years ago

chris-praxis commented 3 years ago

Home.vue.zip CanvasTest.vue.zip

In this simple test, Android graphics BufferQueue is lost after switching pages/tabs. It's not a full working demo but should be very easy to repro with what I've provided.

  1. After timeout, orange draws over blue as expected (both platforms).

  2. Page is unloaded and reloaded via tab navigation (BottomNavigation & TabContentItem)... iOS: Canvas appears the same, orange rectangle in blue. Android: Canvas has reverted to all blue (background color), and when 'fillRect' is called again I get errors...

06-23 15:42:01.878 15438 15438 I JS : 'draw canvas: Canvas 960x1563' 06-23 15:42:01.884 15438 15752 E BufferQueueProducer: SurfaceTexture-0-15438-0 dequeueBuffer: BufferQueue has been abandoned 06-23 15:42:01.884 15438 15752 E BufferQueueProducer: SurfaceTexture-0-15438-0 dequeueBuffer: BufferQueue has been abandoned 06-23 15:42:01.884 15438 15752 E GLContext: Cannot swap buffers!

NS 6 + Vue tns version 8.0.2 Dependencies:

┌──────────────────────────────┬────────────┐
│ Plugin                       │ Version    │
│ @nativescript/canvas         │ ^0.9.22-v6 │
│ @nativescript/email          │ ^2.0.4     │
│ crypto-js                    │ ^3.3.0     │
│ lodash.clonedeep             │ ^4.5.0     │
│ lodash.debounce              │ ^4.0.8     │
│ lodash.merge                 │ ^4.6.2     │
│ nativescript-in-app-purchase │ ^1.0.1     │
│ nativescript-plugin-firebase │ ^10.6.3    │
│ nativescript-shadowed-label  │ ^1.0.0     │
│ nativescript-theme-core      │ ~1.0.6     │
│ nativescript-vue             │ ~2.4.0     │
│ tns-core-modules             │ ^6.5.27    │
│ uglifyjs-webpack-plugin      │ ^2.2.0     │
└──────────────────────────────┴────────────┘

Dev Dependencies:

┌────────────────────────────────────┬─────────┐
│ Plugin                             │ Version │
│ @babel/core                        │ ~7.1.0  │
│ @babel/preset-env                  │ ~7.1.0  │
│ atob                               │ ^2.1.2  │
│ babel-loader                       │ ~8.0.0  │
│ nativescript-dev-webpack           │ ^1.5.1  │
│ nativescript-vue-template-compiler │ ~2.4.0  │
│ node-sass                          │ ^4.14.1 │
│ serialize-javascript               │ ^5.0.1  │
│ tns-android                        │ 6.5.3   │
│ tns-ios                            │ 6.5.3   │
│ typescript                         │ ^3.8.3  │
│ vue-loader                         │ ~15.4.0 │
└────────────────────────────────────┴─────────┘
chris-praxis commented 3 years ago

CanvasTest.vue.zip This version creates a new Canvas ever time tab page is entered/loaded instead of defining via XML, but it results in the same errors when drawing canvases after the first one. Looks like a stale BufferQueue is still accessed, even though it's a new canvas.?

triniwiz commented 3 years ago

CanvasTest.vue 2.zip I would go about it more like

triniwiz commented 3 years ago

Hey can you try the latest alpha tag an lmk

darkyelox commented 3 years ago

Hey can you try the latest alpha tag an lmk

This is happening with alpha too, simply draw a rectangle in a canvas inside of a tab using the @nativescript-community/ui-material-bottom-navigation, navigate to the next tab and back to the first one and the canvas is empty.

I tested using a (tap) event for redraw the canvas when "clicking", if I navigate to a tab and then go back to the first tab (the one with the canvas) and "click it" for call my draw function it throws GLContext: Cannot swap buffers!, if the first time I tap the component for redraw (without navigating to another tab) it draws and no error is thrown.

I solved the problem (temporally) using loaded and unloaded from the parent component elementRef.nativeView.on and *ngIf from Angular over the Canvas HTML element fortunately I created a canvas abstract component for that, for anyone that wants to use it, here is:

import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
import { Canvas } from '@nativescript/canvas'
import { AndroidApplication, ContentView, View } from '@nativescript/core'
import { android } from '@nativescript/core/application'
import { Console } from '@shared/tools'

@Component({
    template: ``
})
export abstract class CanvasBaseComponent extends ContentView implements AfterViewInit {

    public showCanvas = false

    @ViewChild('canvas')
    set canvasRef(canvasRef: ElementRef<Canvas>) {
        this._canvas = canvasRef?.nativeElement
    }

    private _canvas: Canvas

    constructor(
        readonly elementRef: ElementRef<View>
    ) {
        super()
    }

    ngAfterViewInit(): void {
        this.setupCanvas()
    }

    get canvas() {
        return this._canvas
    }

    private setupCanvas() {
        this.elementRef.nativeElement.on('loaded', () => {
            this.showCanvas = true
            this.onCanvasLoaded(this._canvas)
        })

        this.elementRef.nativeElement.on('unloaded', () => {
            this.showCanvas = false
            this.onUnloaded()
        })
    }

    public destroyCanvas() {
        this._canvas.disposeNativeView()
    }

    public onCanvasReady(event) {
        const canvas = event.object as Canvas

        setTimeout(() => this.onDraw(canvas))
    }

    public abstract onDraw(canvas: Canvas)

    public onCanvasLoaded(canvas: Canvas) {

    }

    public onCanvasUnloaded() {

    }

    public redraw() {
        setTimeout(() => {
            if (this._canvas) {
                this.onDraw(this._canvas)
            }
        })
    }
}

Use like this:

import { AfterViewInit, Component, ElementRef, Input, OnDestroy, OnInit } from '@angular/core'
import { localizeStructureMap } from '@config'
import { registerElement } from '@nativescript/angular'
import { Canvas } from '@nativescript/canvas'
import { Color, CSSType, View } from '@nativescript/core'
import { toDevicePixels } from '@nativescript/core/utils/layout-helper'
import { CanvasBaseComponent } from '@shared/abstract-classes'
import { degreesToRadians, getViewConstraints } from '@shared/tools'
import anime from 'animejs'

const componentName = 'AppAcrDynamicKey'

@CSSType(componentName)
@Component({
  selector: componentName,
  templateUrl: './acr-dynamic-key.component.html',
  styleUrls: ['./acr-dynamic-key.component.scss']
})
export class AcrDynamicKeyComponent extends CanvasBaseComponent implements OnInit, OnDestroy, AfterViewInit {

  public localizePath = localizeStructureMap.modules.AcrModule.components.AcrDynamicKey

  @Input() public timeSeconds: number = 5

  @Input() public updateIntervalMillis: number = 100

  @Input() public start = true

  public componentName = componentName

  public dyanamicKey: string

  private clockTimerAnimationProps = {
    // timer start angle in radians (90º)
    clockTimerStartAngle: -90, // Math.PI/2,
    // timer end angle in degrees
    clockTimerEndAngle: -90 // Math.PI/2
  }

  private clockTimerAnimation: anime.AnimeInstance

  constructor(
    readonly elementRef: ElementRef<View>
  ) {
    super(elementRef)
  }

  ngOnInit(): void {
    // TODO: get real key
    const generateKey = () => {
      this.dyanamicKey = Math.random().toString().slice(2, 8)
    }

    this.clockTimerAnimation = anime({
      targets: this.clockTimerAnimationProps,
      clockTimerEndAngle: 270,
      duration: this.timeSeconds * 1000,
      autoplay: false,
      loop: true,
      easing: 'linear',
      update: () => {
        // console.log(Math.ceil(this.clockTimerAnimationProps.clockTimerEndAngle))
        this.redraw()
      },
      loopComplete: () => {
        // TODO: get real key
        generateKey()
      }
    })

    generateKey()
  }

  ngOnDestroy(): void {
    console.log('onDestroy')
  }

  ngAfterViewInit(): void {
    super.ngAfterViewInit()

    if (this.start) {
      setTimeout(() => {
        this.clockTimerAnimation.play()
      })
    }
  }

  public onDraw(canvas: Canvas) {
    const canvasContext = canvas.getContext('2d') as unknown as CanvasRenderingContext2D

    const { width, height } = getViewConstraints(canvas as any)

    const clockBorderWidth = toDevicePixels(6)
    const clockX = width / 2 + toDevicePixels(5)
    const clockY = height / 2
    const clockRadius = height / 2 - toDevicePixels(12)

    const { clockTimerStartAngle, clockTimerEndAngle } = this.clockTimerAnimationProps

    canvasContext.clearRect(0, 0, width, height)
    canvasContext.setTransform(1, 0, 0, 1, 0, 0)
    // canvasContext.save()

    canvasContext.strokeStyle = new Color("white").hex
    canvasContext.fillStyle = new Color("white").hex
    canvasContext.lineWidth = clockBorderWidth
    canvasContext.beginPath()

    // clock form
    canvasContext.arc(clockX, clockY, clockRadius, 0, 2 * Math.PI)
    canvasContext.stroke()

    // clock button
    canvasContext.moveTo(clockX, clockY - clockRadius)
    canvasContext.fillRect(clockX - clockBorderWidth / 2, clockY - clockRadius - clockBorderWidth, clockBorderWidth, clockBorderWidth)

    canvasContext.closePath()

    canvasContext.beginPath()
    // timer arc
    canvasContext.arc(clockX, clockY, clockRadius - (clockBorderWidth + 3), degreesToRadians(clockTimerStartAngle), degreesToRadians(Math.ceil(clockTimerEndAngle)), true)
    canvasContext.lineTo(clockX, clockY)

    canvasContext.fill()

    canvasContext.closePath()

    canvasContext.save()

  }

  onCanvasLoaded(canvas: Canvas) {

  }

  onCanvasUnloaded() {

  }

}

registerElement(componentName, () => require('./acr-dynamic-key.component').AcrDynamicKeyComponent)

the template:

<GridLayout id="container" width="100%" height="65" columns="100, *" (tap)="redraw()">
    <StackLayout col="0">
        <Canvas *ngIf="showCanvas" #canvas width="100%" height="100%" (ready)="onCanvasReady($event)"></Canvas>
    </StackLayout>
    <StackLayout col="1">
        <Label id="title" class="text-white text-sm ml-1" [text]="localizePath.title | L"></Label>
        <Label id="key" class="text-white text-lg font-bold tracking-wide" [text]="dyanamicKey"></Label>
    </StackLayout>
</GridLayout>
triniwiz commented 3 years ago

Please try again using {N} 8.1

darkyelox commented 3 years ago

@triniwiz tested using NS 8.1 and canvas 1.0.0-debug.0 and the same problem: the canvas disappears while a navigation to another view is being performed on Android, I'm using Angular 12 with RootLayout as my wrapper for page-router-outlet btw, so the canvas becomes invisible when nsRouterLink is used, still needing the workaround that i have in previous comment.

triniwiz commented 3 years ago

You should stick with the alpha, debug is an actual debug version of the libs with the symbols included

triniwiz commented 2 years ago

Can you try with the latest , please reopen if the issue still persists