How to correctly handle nested scroll views in Nativescript/Angular - user-interface

I'm trying to create in Nativescript(Testing it on Android) a view containing a Donut Chart and an Accordion-like list below it, and enable infinite scrolling, so that when i scroll down the chart will scroll up out of the view, leaving the whole screen available for the list.
The problem is that no matter what i try the accordion-list will either scroll out of the view, leaving the chart on top of it, basically appearing like it's hiding behind the chart, or the elements won't display correctly / at all.
This is how my layout is made so far
Index screen
<ScrollView heiht="100%>
<StackLayout>
<DonutChart [chartDataIterable]="chartData"></DonutChart>
<Accordion [items]="items"></Accordion>
</StackLayout>
</ScrollView>
Donut Chart Component
<GridLayout rows="auto">
<RadPieChart row="0" height="300" allowAnimation="true" (pointSelected)="changeDisplayValue($event)" (pointDeselected)="resetToTotal($event)">
<DonutSeries tkPieSeries seriesName="dataSeries" selectionMode="DataPoint" outerRadiusFactor="0.9" expandRadius="0.4"
outerRadiusFactor="0.7" innerRadiusFactor="0.7" [items]="chartDataObservable" valueProperty="value" legendLabel="type"></DonutSeries>
</RadPieChart>
<StackLayout row="0" horizontalAlignment="center" verticalAlignment="center">
<Label horizontalAlignment="center" [text]="currentType"></Label>
<Label horizontalAlignment="center" [text]="currentTypeAmount"></Label>
</StackLayout>
</GridLayout>
Accordion Component
<ListView [items]="items" height="100%">
<ng-template let-item="item">
<AccordionCell [item]="item"></AccordionCell>
</ng-template>
</ListView>
The GridLayout wrapping the chart is used to add some info at the center of the donut, and the accordion height 100% is set to prevent the list view from taking the height of a single cell.
I suspect that the issue is due to ListView integrating a ScrollView by default, thus prioritizing the scrolling in the ListView and never triggering the scrolling of the outer ScrollView since the content never exceeds the screen size.

Since there hasn't been an answer for a week now i'll share the workaround i came up with.
Instead of using a ScrollView to scroll through my chart and accordion(basically ListView) components, i've moved the Chart and positioned it as the first cell of the ListView, this way both scrolling and cell recycling work well. The only drawback is that a little extra cell management is needed in order to make sure that the chart stays on top, without losing the first entry of the list's data source.
accordion.component.ts
ngOnInit(): void {
this.observableItems = new ObservableArray(this.items);
this.observableItems.unshift({});
}
public templateSelector (item: any, index: number, items: ObservableArray<any>){
if(index === 0)
return "chart";
else if(item.date != items.getItem(index-1).date)
return "date";
else
return "default";
}
accordion.component.html
<RadListView [items]="observableItems" [itemTemplateSelector]="templateSelector" (itemLoading)="itemLoading($event)">
<ng-template tkListItemTemplate let-item="item" let-index="index">
<AccordionCell [item]="item" [index]="index" [itemLoadedEvent]="itemLoadedEvent.asObservable()"></AccordionCell>
</ng-template>
<ng-template tkTemplateKey="date" let-item="item" let-index="index">
<StackLayout>
<Label [text]="item?.date | date: 'dd/MM/yyyy'"></Label>
<AccordionCell [item]="item" [index]="index" [itemLoadedEvent]="itemLoadedEvent.asObservable()"></AccordionCell>
</StackLayout>
</ng-template>
<ng-template tkTemplateKey="chart">
<DonutChart [chartDataIterable]="chartDataIterable (chartSectionSelected)="chartSectionSelectedHandler($event)" (chartSectionDeselected)="chartSectionDeselectedHandler()"></DonutChart>
</ng-template>

Related

Nativescript - having a scrollView in a GridLayout not working on iOS

I'm trying to create a layout that has a button row at the top and then a scrollView beneath that:
<GridLayout columns="*,40,10" rows="10,40,*">
<Label row="1" col="1" text="X" tap="close" />
<ScrollView orientation="vertical" col="0" row="2" colSpan="3">
…
</ScrollView>
</GridLayout>
This works on Android, I get a ScrollView that's 50 from the top and the Label with the «X» in it on top.
On iOS, the ScrollView always is at the top of the view, overlapping the «X»-Label.
Any ideas what I'm doing wrong?
NS 8.x
I suspect the issue is that you have two rows but reference them as 1 and 2 rather than 0 and 1.

iOS buttons become unclickable after making a floating layout

I'm creating a custom action bar and my idea is that the actionbar must be transparent (thats why I can't/don't want to use the native one) and behind the action bar there must be content (an image in this case).
I tried multiple ways and all of them with the same result, the iOS button become unclickable for no reason.
Ok, so I have an AbsoluteLayout inside I have GridLayout fixed on top as the custom actionbar and then a ScrollView with 100% width and height, inside the scrollview there's some content. The problem is as soon as I put a button inside the GridLayout this become unclickable only on iOS because in Android works just fine.
Let me show you my example with some code:
<Page actionBarHidden="true">
<AbsoluteLayout>
<GridLayout rows="50, *" backgroundColor="red" top="0" left="0" height="50" id="bar">
<Button text="click" #tap="goToDetailPage" id="buttondelsous"></Button>
</GridLayout>
<ScrollView backgroundColor="blue" width="100%" height="100%" top="0" id="content">
<WrapLayout>
<StackLayout>
<Label text="this is behind"></Label>
</StackLayout>
</WrapLayout>
</ScrollView>
</AbsoluteLayout>
</Page>
As for the styles forcing the custom actionbar being in front of the scrollview I have:
#bar {
z-index: 5;
width: 100%;
}
#content {
z-index: 2;
}
This looks like this on Android:
Same way on iOS but the button "Click" is not working, like if it was behind something..
Any idea on how to fix this or any other approach to get what I need? Remember behind the action I must be able to place content (like a background image that I don't want to place in the Page tag itself but in another layout)
Thanks!
A wise man once told me that Nativescript stacks elements in the order you list them. Try flipping around the order so the button is listed last in the template. I believe you won't even need the z-index line if you flip the order around.
should look like
<Page actionBarHidden="true">
<AbsoluteLayout>
<ScrollView backgroundColor="blue" width="100%" height="100%" top="0" id="content">
<WrapLayout>
<StackLayout>
<Label text="this is behind"></Label>
</StackLayout>
</WrapLayout>
</ScrollView>
<!-- I'm listed second so I will be on top even though I have row="0" -->
<GridLayout rows="50, *" backgroundColor="red" top="0" left="0" height="50" id="bar">
<Button text="click" #tap="goToDetailPage" id="buttondelsous"/>
</GridLayout>
</AbsoluteLayout>
</Page>

GridLayout in ScrollView - NativeScript

I am new to NativeScript. I have 4 images in my page and I want to achieve this.
Except I want my cover image to be of full-height of the screen. This is the code I'm trying.
<ScrollView>
<StackLayout height="100%" width="100%">
<GridLayout columns="*, *" rows="5*, *, *">
<StackLayout
:row="0"
:col="0"
:colSpan="2"
class="bgImage coverImage"
:style="{backgroundImage:`url('${defaultImg}')`}"
></StackLayout>
<StackLayout
:row="calcRow(idx)"
:col="calcCol(idx)"
:colSpan="idx==0?2:1"
v-for="(item,idx) in event.imgs"
:key="idx"
class="bgImage eventImage"
:style="{backgroundImage:`url('${item}')`}"
></StackLayout>
</GridLayout>
</StackLayout>
</ScrollView>
If I didn't give height="100%" to StackLayout it's not even loading the images. If I do give it then there is no scroll.
Here is the playground link Playground Link
You may use the layoutChanged event of your ScrollView where you may calculate the height of it and use it as height for the first image.
Playground Sample

How to position element at the bottom of Absolute Layout in NativeScript?

I want to position an element at the bottom of the screen in Absolute Layout in NativeScript.
I have this code:
<AbsoluteLayout>
<maps:mapView
left="0"
top="0"
width="100%"
height="100%"
latitude="{{ map.latitude }}"
longitude="{{ map.longitude }}"
zoom="{{ map.zoom }}"
padding="{{ map.padding }}"
mapReady="onMapReady"
coordinateTapped="onCoordinateTapped"
markerSelect="onMarkerSelect"
shapeSelect="onShapeSelect"
cameraChanged="onMapCameraChanged"/>
<ScrollView
left="0"
top="0"
width="100%"
orientation="horizontal">
<!-- More XML -->
</ScrollView>
<StackLayout
left="0"
bottom="0"
width="100%"
visibility="visible"
orientation="horizontal"
style="background-color: red;">
<Label text="TITLE"></Label>
</StackLayout>
</AbsoluteLayout>
I figured out that there is no bottom attribute for AbsoluteLayout... Here is the picture of what I want to create:
So how to arange items like in the picture, especially the bottom one?
EDIT: I should note that dimensions of this bottom rectangle may not be always same....
I did something similar one day, programmatically & with Angular, maybe this can help.
If you don't want to use a GridLayout you can try to get height of your bottom element and of the screen, then place your element from the top with a simple calcul : screen's height - bottom element's height (- more if you want some padding). You can use two types of values : DIPs and pixels. If you're using pixels, you need to convert your values into DIPs by using the screen scale.
Something like this (I didn't test the code I'm giving you, it's just an example) :
1] add an id to your bottom element so you can access it inside your component :
<StackLayout #bottomElt></StackLayout>
2] update your component to set element position inside your absolute layout
// you need ElementRef, OnInit and ViewChild
import { Component, ElementRef, OnInit, ViewChild, ViewContainerRef } from "#angular/core";
import { AbsoluteLayout } from "ui/layouts/absolute-layout";
import { StackLayout } from "ui/layouts/stack-layout";
// you need access to screen properties
import { screen } from "tns-core-modules/platform";
[...]
export class YourComponent implements OnInit {
// add access to element inside your component
#ViewChild("bottomElt") bottomElt: ElementRef;
// create variable to access bottom element properties
bottomContainer: StackLayout;
// set bottom element position after view init
// example : inside ngOnInit function (for Angular version)
ngOnInit(): void {
this.bottomContainer = <StackLayout>this.bottomElt.nativeElement;
// using DIPs values only
AbsoluteLayout.setTop(this.bottomContainer, (screen.mainScreen.heightDIPs - Number(this.bottomContainer.height)));
// using pixels and screen scale
// this way you can get height without knowing it
AbsoluteLayout.setTop(this.bottomContainer, (screen.mainScreen.heightDIPs - (Number(this.bottomContainer.getMeasuredHeight()) / screen.mainScreen.scale)));
}
More information about screen values : https://docs.nativescript.org/api-reference/interfaces/platform.screenmetrics.html
Alternative way
Instead of using AbsoluteLayout, you can use a GridLayout to set a bottom bar, with two rows : one with a wildcard size and the other with auto size so it can fit your bottom bar height everytime it changes. I did it this way in a mobile application to get a menu at the bottom in Android and IOS :
<GridLayout rows="*, auto" width="100%">
<AbsoluteLayout row="0" orientation="vertical">
<!-- YOUR CONTENT (maps & ScrollView) -->
</AbsoluteLayout>
<!-- YOUR BOTTOM BAR (StackLayout). Don't forget to add row="1" -->
<StackLayout #bottomElt row="1">[...]</StackLayout>
</GridLayout>
Another option is using FlexboxLayout in your AbsoluteLayout container like this:
<FlexboxLayout flexDirection="column" justifyContent="space-between" height="100%">
<ScrollView
width="100%"
orientation="horizontal">
<!-- More XML -->
</ScrollView>
<StackLayout
width="100%"
visibility="visible"
orientation="horizontal"
style="background-color: red;">
<Label text="TITLE"></Label>
</StackLayout>
</FlexboxLayout>
This is the absolute best solution, got it from one of the devs: https://github.com/NativeScript/NativeScript/issues/5591#issuecomment-482640921
<GridLayout rows="*,auto">
<ItemTakingFullScreen rowSpan="2"/>
<ItemShownUnder row="1"/>
<ItemShownAbove row="1">
</GridLayout>
Basically, you can use grid layout and have a item take up multiple grid spaces, sharing them with some other item.
here is the best solution
wrapper all the elements in an absolutelayout with width and hieght to 100% and maybe add a gridlayout to hold the main content .
<AbsoluteLayout width='100%' height='100%'>
<StackLayout width='100%' hieght='100%' left='0' top='0'>
//add you structure here
</StackLayout>
add your fixed element here
<image src='add the float item'/>
</AbsoluteLayout>
It can be done also with GridLayout:
<GridLayout rows="16,*,16" columns="16,*,16" width="100%" backgroundColor="red">
<GridLayout row="1" col="1" rows="auto, auto, auto" columns="auto" horizontalAlignment="right" verticalAlignment="bottom" backgroundColor="blue">
<!-- Your content at bottom right corner -->
<Label row="0" text="Your content" textAlignment="center" textWrap="true"></Label>
<Label row="1" text="at" textAlignment="center" textWrap="true"></Label>
<Label row="2" text="bottom right corner" textAlignment="center"></Label>
</GridLayout>
</GridLayout>
This is the easy way
<DockLayout backgroundColor="lightgray" stretchLastChild="true">
<Label text="top" dock="top" height="60" backgroundColor="green">
</Label>
<Label text="bottom" dock="bottom" height="60" backgroundColor="yellow"></Label>
<Label text="center" backgroundColor="red"></Label>
</DockLayout>
This is what you want!
<DockLayout backgroundColor="lightgray" stretchLastChild="false">
<Label text="top" dock="top" height="60" backgroundColor="green">
</Label>
<Label text="bottom" dock="bottom" height="60" backgroundColor="yellow"></Label>
</DockLayout>

How to animate an element inside a List Item in Nativescript?

I'm trying to animate an element inside a list item when another item is tapped, but I can't fin a way to get it.
<Repeater items="{{ feedItems }}">
<Repeater.itemTemplate>
<GridLayout>
<GridLayout rows="1*,1*, 1*">
<GridLayout columns="4*,22*">
...
</GridLayout>
<GridLayout row="2">
...
<Image col="2" verticalAlignment="bottom" tap="showElementAndAnimate" />
</GridLayout>
</GridLayout>
<GridLayout cssClass="ElementToShow" columns="1*,1*">
<GridLayout rows="1*,1*,1*" col="1" cssClass="ElementToAnimate">
...
</GridLayout>
</GridLayout>
</GridLayout>
</Repeater.itemTemplate>
</Repeater>
The problem is to get the element to animate after tap since there is a repeater
What you can do is:
Add a attribute (or Id) to your first parent GridLayout.
Add id to the GridLayout that you want to animate (in your case - GridLayout with cssClass="ElementToAnimate").
Access the Image, which you want to tap on, from the args in the function showElementAndAnimate.
Then get the Image parent until you reach the parent with the already specified attribute (from point 1).
When the correct GridLayout is reached, use the function getViewById to get the GridLayout you want to animate (the one with the id from point 2.).
After you get the element, you can animate it.

Resources