Nativescript: Add the ability to animate height and width

Created on 15 Mar 2016  Â·  37Comments  Â·  Source: NativeScript/NativeScript

I would like the ability to animate a UI element’s height and width. Here’s my use case: I have a container that I need to grow after a user interaction. If I use a scale animation on the container, everything in the container (text fields, buttons, labels, etc) scales as well, which I don’t want. I just want the container to grow and everything in the container to keep the same dimensions.

Now I _can_ workaround this by absolutely positioning an element behind my content and scaling that element instead, but that code bloats my XML file, and, it’s _really_ hard to get all the dimensions and positioning of these elements to line up perfectly across devices and screen sizes.

I realize this is not an easy thing to implement, as I’m sure there are layout considerations, but I thought I’d log this limitation to see if others in the community are hitting this problem as well.

css feature

Most helpful comment

Hi folks. You can do the animation with JS and even have some curves if you want. I show how to do this with JS in my Pluralsight course (with the source code available here). The code is for {N}2.5 so you'll have to look through it to get stuff done.

I'm currently working on a nice RxJS solution that's more maintainable.
Here's some sample code to achieve this effect (this is an angular component)

tns-height-demo

The repo where you can download this code:
https://github.com/alexziskind1/tnsheightanimationdemo

The code:

import { Component } from "@angular/core";
import { Observable, Scheduler } from "rxjs";
import { Label } from 'ui/label';

const timeElapsed = Observable.defer(() => {
    const start = Scheduler.animationFrame.now();
    return Observable.interval(1)
        .map(() => Math.floor((Date.now() - start)));
});

const duration = (totalMs) =>
    timeElapsed
        .map(elapsedMs => elapsedMs / totalMs)
        .takeWhile(t => t <= 1);

const amount = (d) => (t) => t * d;

const elasticOut = (t) =>
    Math.sin(-13.0 * (t + 1.0) *
        Math.PI / 2) *
    Math.pow(2.0, -10.0 * t) +
    1.0;

@Component({
    selector: "ns-app",
    template: `
        <StackLayout class="wrapper" (tap)="onTap(lbl)">
            <Label #lbl class="thelabel" text="Hello"></Label>
        </StackLayout>
    `,
    styles: [
        `
            .wrapper {
                text-align: center;
            }
            .thelabel {
                background-color: red;
                color: white;
            }
        `
    ]
})

export class AppComponent {
    onTap(lbl: Label) {
        duration(2000)
            .map(elasticOut)
            .map(amount(250))
            .subscribe(curFrame => {
                lbl.style.height = curFrame;
            },
            er => console.error(er),
            () => console.log('booya!')
            );
    }
}

Or if you want something a little more modular, where you could bind multiple listeners to your observable, you would store the observable in a property that you will use to bind to.

So now your template is like this:

        <StackLayout class="wrapper" (tap)="onTap($event)">
            <Label  class="thelabel1" text="Hello" [height]="blah$ | async"></Label>
            <Label  class="thelabel2" text="Hello" [height]="blah$ | async"></Label>
        </StackLayout>

And your code is just this:

export class AppComponent {
    blah$: Observable<number>;
    onTap() {
        this.blah$ = duration(2000)
            .map(elasticOut)
            .map(amount(150));
    }
}

Which results in this... (i didn't include the new css classes here).

animheight2

All 37 comments

I ran into this last week and would love to see this as well. Aside from my emoticon reaction above, nothing like words of affection :+1:

I've been looking into this for quite a long time. Much appreciated.

👍

+1

👍

+1, yes please, sounds pretty essential for animating list item expanding/collapsing.

:+1:

You can sort of hack this together using the timer and a loop. You won't get curve based animation, but it works in a pinch.

Basically, set the width property of the view you want to scale to an observable, and procedurally increase/decrease it.

Something like this:

function sideBarIn() {
    console.log("starting loop");
    sideOpen = 1;
    pageData.set("hamburgerVis", "collapsed");

    function nextFrame() {
        if (x < 25) {
            x = x + 5;
            xString = x.toString() + "%";
            console.log(xString);
            pageData.set("sideWidth", xString);
            setTimeout(nextFrame, 1);
        }
    }
    // Start the loop
    setTimeout(nextFrame, 0);}

function sideBarOut() {
    sideOpen = 0;
    console.log("starting loop");

    function nextFrame() {
        if (x > 0) {
            x = x - 5;
            if (x == 0) {
                xString = x.toString();
                console.log(xString);
                pageData.set("sideWidth", xString);
            } else {
                xString = x.toString() + "%";
                console.log(xString);
                pageData.set("sideWidth", xString);
            }
            setTimeout(nextFrame, 1);
        }
    }
    // Start the loop
    setTimeout(nextFrame, 0);
}

untitled

Surprisingly smooth on an actual iPad, not so much in the GIF.

@NickArgyle you = winning.

+1 for a built in solution using Animate. I know this is old but I've hit this wall a couple of times now.

Please implement this, would be very usefull to make expanding and collapsing components

+1

Is this that difficult to implement? It's been a year already with no feedback from the core team.
ping @Pip3r4o @hamorphis @hshristov @hdeshev

I know you are focusing on the 3.0 release with lots of stuff to do, but could you point some directions to make this happen? We could contribute with a PR.

Thanks in advance.

@vjoao you can check Alex course on animations while this isn't done

@danielgek How would that help me animating the height property natively? I can do it on plain JS already, but looks hacky and it's not leveraging the hardware accel performance boost.

@vjoao yes he talks about that ;)

@vjoao I am not part of the NativeScript team anymore.

Anyone from the NativeScript team? What's the status on this?

Hi folks. You can do the animation with JS and even have some curves if you want. I show how to do this with JS in my Pluralsight course (with the source code available here). The code is for {N}2.5 so you'll have to look through it to get stuff done.

I'm currently working on a nice RxJS solution that's more maintainable.
Here's some sample code to achieve this effect (this is an angular component)

tns-height-demo

The repo where you can download this code:
https://github.com/alexziskind1/tnsheightanimationdemo

The code:

import { Component } from "@angular/core";
import { Observable, Scheduler } from "rxjs";
import { Label } from 'ui/label';

const timeElapsed = Observable.defer(() => {
    const start = Scheduler.animationFrame.now();
    return Observable.interval(1)
        .map(() => Math.floor((Date.now() - start)));
});

const duration = (totalMs) =>
    timeElapsed
        .map(elapsedMs => elapsedMs / totalMs)
        .takeWhile(t => t <= 1);

const amount = (d) => (t) => t * d;

const elasticOut = (t) =>
    Math.sin(-13.0 * (t + 1.0) *
        Math.PI / 2) *
    Math.pow(2.0, -10.0 * t) +
    1.0;

@Component({
    selector: "ns-app",
    template: `
        <StackLayout class="wrapper" (tap)="onTap(lbl)">
            <Label #lbl class="thelabel" text="Hello"></Label>
        </StackLayout>
    `,
    styles: [
        `
            .wrapper {
                text-align: center;
            }
            .thelabel {
                background-color: red;
                color: white;
            }
        `
    ]
})

export class AppComponent {
    onTap(lbl: Label) {
        duration(2000)
            .map(elasticOut)
            .map(amount(250))
            .subscribe(curFrame => {
                lbl.style.height = curFrame;
            },
            er => console.error(er),
            () => console.log('booya!')
            );
    }
}

Or if you want something a little more modular, where you could bind multiple listeners to your observable, you would store the observable in a property that you will use to bind to.

So now your template is like this:

        <StackLayout class="wrapper" (tap)="onTap($event)">
            <Label  class="thelabel1" text="Hello" [height]="blah$ | async"></Label>
            <Label  class="thelabel2" text="Hello" [height]="blah$ | async"></Label>
        </StackLayout>

And your code is just this:

export class AppComponent {
    blah$: Observable<number>;
    onTap() {
        this.blah$ = duration(2000)
            .map(elasticOut)
            .map(amount(150));
    }
}

Which results in this... (i didn't include the new css classes here).

animheight2

will this work for LIstview items??? @alexziskind1

For those who wants the Rxjs5.5 version ( without loading everyting from rxjs) :

import {defer}          from 'rxjs/observable/defer';
import {interval}       from 'rxjs/observable/interval';
import {map}            from 'rxjs/operators/map';
import {takeWhile}      from 'rxjs/operators/takeWhile';
import {animationFrame} from 'rxjs/scheduler/animationFrame';

const timeElapsed = defer(() =>
                          {
                              const start = animationFrame.now();
                              return interval(1)
                                  .map(() => Math.floor((Date.now() - start)));
                          });
export const durationForAnimation = (totalMs) => timeElapsed.let(map((elapsedMs: number) => elapsedMs / totalMs))
                                                            .let(takeWhile(t => t <= 1));
export const amount = (d) => (t) => t * d;
export const elasticOut = (t) => Math.sin(-13.0 * (t + 1.0) * Math.PI / 2) * Math.pow(2.0, -10.0 * t) + 1.0;

+1

for anyone interested I wanted that without using rxjs.
I ended up using tween.js which is dead small.
I created a very simple animation.ts file

import * as TWEEN from '@tweenjs/tween.js';
export { Easing } from '@tweenjs/tween.js';
export class Animation extends TWEEN.Tween {
    constructor(obj) {
        super(obj);
        this['_onCompleteCallback'] = function() {
            cancelAnimationFrame();
        };
    }
    start(time?: number) {
        startAnimationFrame();
        return super.start(time);
    }
}

export function createAnimation(duration:number) {

}

let animationFrameRunning = false;
const cancelAnimationFrame = function() {
    runningTweens--;
    if (animationFrameRunning && runningTweens === 0) {
        animationFrameRunning = false;
    }
};

let runningTweens = 0;
const startAnimationFrame = function() {
    runningTweens++;
    if (!animationFrameRunning) {
        animationFrameRunning = true;
        tAnimate();
    }
};
const requestAnimationFrame = function(cb) {
    return setTimeout(cb, 1000 / 60);
};
function tAnimate() {
    if (animationFrameRunning) {
        requestAnimationFrame(tAnimate);
        TWEEN.update();
    }
}

which I can then use like this:

new Animation.Animation({ width: 0 })
            .to({ width: 84 }, 200)
            .easing(Animation.Easing.Quadratic.Out)
            .onUpdate(obj => {
                this.stopBtn.nativeElement.style.scaleX = obj.width / 84;
                this.stopBtn.nativeElement.style.scaleY = obj.width / 84;
                this.stopBtnHolder.nativeElement.style.width = this.stopBtnHolder.nativeElement.style.height = obj.width;
            })
            .start();

Still needs improvements (especially on the complete callback) but already works reallyy well

@farfromrefug I had to make one adjustment for some reason, maybe this was a newly introduced enhancement from tween. I had to override the definition of TWEEN.now because the logic they are using rightly assumes it is a node environment, but it looks like process.hrtime() is not available in {N}.

TWEEN.now = function () {
  return new Date().getTime();
};

@akoumjian You should not be needing that as Tween test for process and window. As you should not have any of those Tween will use Date.now

The actual check only looks to see if process is defined, and then assumes
hrtime exists.
On Tue, Sep 25, 2018 at 03:44 Martin Guillon notifications@github.com
wrote:

@akoumjian https://github.com/akoumjian You should not be needing that
as Tween test for process and window. As you should not have any of those
Tween will use Date.now

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/NativeScript/NativeScript/issues/1764#issuecomment-424240226,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAszJJ8D6wdVsZIKMTUB3L1pS2jfVM2yks5ued7RgaJpZM4HxCgv
.

Yes but you should not have process defined! So it should never go there.

I mean, I didn’t create the runtime. I suppose some third party package
could add it to the environment.
On Tue, Sep 25, 2018 at 08:37 Martin Guillon notifications@github.com
wrote:

Yes but you should not have process defined! So it should never go there.

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
https://github.com/NativeScript/NativeScript/issues/1764#issuecomment-424325840,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAszJG2kskG62u2Mmqa-z7wDFcuPpIC0ks5ueiOjgaJpZM4HxCgv
.

Updated for rxjs 6:

import { defer, interval, animationFrameScheduler } from 'rxjs';
import { map, takeWhile } from 'rxjs/operators';

const timeElapsed = defer(() => {
  const start = animationFrameScheduler.now();
  return interval(1).pipe(map(() => Math.floor((Date.now() - start))));
});

export const durationForAnimation = (totalMs) => timeElapsed.pipe(map((elapsedMs: number) => elapsedMs / totalMs), takeWhile(t => t <= 1));
export const amount = (d) => (t) => t * d;
export const elasticOut = (t) => Math.sin(-13.0 * (t + 1.0) * Math.PI / 2) * Math.pow(2.0, -10.0 * t) + 1.0;

onTap:

onTap(lbl: Label) {
    durationForAnimation(2000).pipe(map(elasticOut), map(amount(50))).subscribe((heightValue) => {
      lbl.style.height = heightValue;
    });
  }

How can I have this without height specificed? e.g. dynamic height. Not all the time we know the height of the content, sometime we may just want it to expand dynamically.

@mannok You can do this by rendering the content with display:none, then calculating its height.

@ryandavie Do you mean visibility: collapsed? as I cannot find display: none is working in {N}.
If yes, I have tried just now, seems it is not working. All these: nativeView.height, nativeView.getMeasuredHeight(), nativeView.getActualSize() give me 0 for height when the container is with visibility: collapsed

@mannok Sorry I was speaking broadly about how to achieve it. It has been a long time and I can't find the code I was working with.

What if you set a timeout to give the content time to render, then use nativeView.getMeasuredHeight()?

@ryandavie Haha, nvm. Seems that the system can getMeasuredHeight() correctly only if the content is fully shown. Otherwise, the measured content size will be cropped by its parent, so no matter I set timeout or not. I cannot get the content original height until it is fully shown.

I tried to set opacity to 0 first.

    nativeView.opacity=0;
    nativeView.addEventListener("loaded", () => {
          setTimeout(() => {
            let size = nativeView.getActualSize();
            //console.log("loaded",size);
            nativeView.style.height=0;
            nativeView.opacity=1;

            // start animation now

         });
   });

@ryandavie Haha, nvm. Seems that the system can getMeasuredHeight() correctly only if the content is fully shown. Otherwise, the measured content size will be cropped by its parent, so no matter I set timeout or not. I cannot get the content original height until it is fully shown.

How did you solve this problem? I also have dynamic content, but the getMeasuredHeight () method does not work correctly.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

NordlingDev picture NordlingDev  Â·  3Comments

dhanalakshmitawwa picture dhanalakshmitawwa  Â·  3Comments

minjunlan picture minjunlan  Â·  3Comments

guillaume-roy picture guillaume-roy  Â·  3Comments

rogangriffin picture rogangriffin  Â·  3Comments