Overlay positioning in NativeScript - nativescript

I'm working on a nativescript plugin for a material-inspired dropdown list of sorts. The plugin adds an AbsoluteLayout to the Page to act as a backdrop, then adds a ListView to that AbsoluteLayout so that the picker is displayed on top. It also adds a GridView with label and such that gets put in the position defined by its XML. Here's an example of the plugin in use in a view's XML:
<GridLayout rows="auto" columns="*, auto">
<StackLayout>
<label text="Color" />
<label style="height: 3; background-color: gray;" />
</StackLayout>
<MDL:MaterialDropdownList col="1" id="ddlColors"
items="{{ colors }}" selectedIndex="{{ selectedColorIndex }}" >
</MDL:MaterialDropdownList>
</GridLayout>
What I'm wanting to do is display the ListView directly over the MaterialDropdownList element when it's tapped. However, I've tried going by the originX and originY, all the way up through each parent element until I hit the Page, and I get back a super low number of "2". Here's a snippet:
let src: viewModule.View = <viewModule.View>arg.object,
x: number, y: number;
x = src.originX;
y = src.originY;
let parent = src.parent;
while (parent !== this.page) {
x += parent.originX;
y += parent.originY;
parent = parent.parent;
}
console.log(`x and y are ${x} and ${y}`); //2, 2
this._listPicker.originX = x;
this._listPicker.originY = y;
Any thoughts greatly appreciated!

I believe the function you are looking for is element.getLocationOnScreen()
or element.getLocationInWindow() to get the actual position of the element.

Related

Xamarin forms breadcrumbs with horizontal scroll layout

I have a xamarin.forms app in which I am trying achieve a specific UI.Please find the attched image.
.
As you can see It is a list view and have a breadcrumbs below it. What I am trying to achieve is when user click any of the other items such as "stores" or "users" in breadcrumbs, then the upper layout horizontally slide and show another list view.Where I am stuck is I want to fix the breadcrumbs at the bottom and the change only needs the upper layout i.e.; the list view layout. How can I achieve this. Any ideas will be much helpfull.
What I am thinking is putting four listview inside horizontal scroll view.But is it the better approach?
This could be achieved by simple Translate animation.
A simple implementation of the idea of using translation. Change as per need.
XAML layout:
<StackLayout>
<Grid x:Name="rotatingView">
<ListView
...../>
<ListView
TranslationX="{Binding Width, Source={x:Reference rotatingView}}"
...../>
<ListView
TranslationX="{Binding Width, Source={x:Reference rotatingView}}"
...../>
<ListView
TranslationX="{Binding Width, Source={x:Reference rotatingView}}"
...../>
</Grid>
<Button
Text="0"
Clicked="Button_Clicked"/>
<Button
Text="1"
Clicked="Button_Clicked"/>
<Button
Text="2"
Clicked="Button_Clicked"/>
<Button
Text="3"
Clicked="Button_Clicked"/>
</StackLayout>
Xaml.cs clicked:
int previousSelectedIndex = 0;
private async void Button_Clicked(System.Object sender, System.EventArgs e)
{
Button selectedtab = (sender as Button);
int selectedViewIndex = int.Parse(selectedtab.Text);
VisualElement previousView = rotatingView.Children[previousSelectedIndex];
VisualElement selectedView = rotatingView.Children[selectedViewIndex];
bool isMovingForward = true;
if (previousSelectedIndex < selectedViewIndex)
{
isMovingForward = true;
}
else if(previousSelectedIndex > selectedViewIndex)
{
isMovingForward = false;
}
if (selectedViewIndex != previousSelectedIndex)
{
selectedView.TranslationX = rotatingView.Width * (isMovingForward ? 1 : -1);
await Task.WhenAll(
selectedView.TranslateTo(0, 0),
previousView.TranslateTo(rotatingView.Width * (isMovingForward ? -1 : 1), 0));
}
this.previousSelectedIndex = selectedViewIndex;
}
Here I have used the text of buttons to select index of the view. Hope this could help.
if you are looking for a breadcrumb navigation control.
I have created a control that will generate one automatically, and it's highly customisable.
https://github.com/IeuanWalker/Xamarin.Forms.Breadcrumb

How to make ActivityIndicator overlay full screen?

I have a StackLayout and a number of elements inside (buttons, texts etc).
I want the ActivityIndicator to overlay the entire screen and make it not able to do anything to those elements.
I have put ActivityIndicator inside the StackLayout but wrapped it with AbsoluteLayout thinking that AbsoluteLayout can easitly overlap everything:
<StackLayout>
<AbsoluteLayout>
<ActivityIndicator ... />
</AbsoluteLayout>
<...other elements...>
</StackLayout>
Instead activity indicator is displayed at the top of the StackLayout and other elements are available for affecting. I'm new in Xamarin and layouts, what am I doing wrong? All samples in the Internet have single ActivityIndicator per page...
It is better said that an AbsoluteLayout's children can easily overlap each other. Just as a StackLayout lets you stack controls inside , vertically or horizontally, an AbsoluteLayout lets you position controls inside using absolute or proportional values, thus if two controls have the same absolute positioning set, they will overlap 100%.
Therefore, you want to wrap your StackLayout and another StackLayout that has your ActivityIndicator inside an AbsoluteLayout using proportional sizing, e.g:
<AbsoluteLayout>
<StackLayout
x:Name="mainLayout"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All" >
<Label Text="Welcome to Xamarin.Forms!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand" />
<Button Text="Do Something"
Clicked="DoSomethingBtn_Clicked" />
</StackLayout>
<StackLayout
x:Name="aiLayout"
IsVisible="False"
AbsoluteLayout.LayoutBounds="0,0,1,1"
AbsoluteLayout.LayoutFlags="All"
BackgroundColor="Gray" Opacity="0.5">
<ActivityIndicator
x:Name="ai"
IsRunning="False"
HorizontalOptions="CenterAndExpand"
VerticalOptions="CenterAndExpand"
Color="Black"/>
</StackLayout>
</AbsoluteLayout>
The above sets the two StackLayouts to both take up the full size of the parent container of the AbsoluteLayout, which is presumably a Page. The StackLayout that has the indicator is initially hidden. IN the page code behind for the above example, I show the second StackLayout and start the activity indicator and show it for 2 seconds, and then hide it again:
private async void DoSomethingBtn_Clicked(object sender, EventArgs e)
{
ai.IsRunning = true;
aiLayout.IsVisible = true;
await Task.Delay(2000);
aiLayout.IsVisible = false;
ai.IsRunning = false;
}
Here is what it looks like:
And since the second StackLayout completely covers the first, none of the controls in the first StackLayout are clickable.
Might be worth going over the docs for the AbsoluteLayout to understand the AbsoluteLayout.LayoutBounds and AbsoluteLayout.LayoutFlags:
https://learn.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/layouts/absolute-layout
If you want to "overlap", you need to be outside of the StackLayout. A Grid is the most common control for this:
<Grid>
<StackLayout>
<...other elements...>
</StackLayout>
<ActivityIndicator ... />
</Grid>
Here's a hacked-up control for making things full-screen via the horribly-named RelativeLayout (tested in Android only)
[ContentProperty("ContentInner")]
public class FullScreenLayout : ContentView
{
public View ContentInner
{
get => ((RelativeLayout) Content).Children[0];
set
{
var display = DeviceDisplay.MainDisplayInfo;
var screenWidth = display.Width / display.Density;
var screenHeight = display.Height / display.Density;
var wrapper = new RelativeLayout();
wrapper.Children.Add(value, () => new Rectangle(0, 0, screenWidth, screenHeight));
Content = wrapper;
}
}
}
It can be used like this:
<controls:FullScreenLayout>
<!-- Anything you want fullscreen here -->
</controls:FullScreenLayout>
Unfortunately, if you use NavigationPage, this won't overlap the navigation bar. Every other solution currently on this page has the same issue. According to this question, it's not possible to solve this without using platform-specific customer renderers. Ugh.
If you don't mind the page being dimmed, you can use Rg.Plugins.Popup which implements the custom renderers needed.
I ended up solving my similar problem (dimming most of the screen) by implementing a custom renderer for the navigation page itself.

Determine If component is visible in the screen

I use Nativescript + Angular and this is my code:
<ScrollView class="body" id="scroll" #scroll (scroll)="scrollEvent($event);">
<StackLayout #stackScroll>
<ng-template ngFor let-card [ngForOf]="allList">
<StackLayout [card]="card">
<my-custom-component [aCard]="card"></my-custom-component>
</StackLayout>
</ng-template>
</StackLayout>
</ScrollView>
I have used this snippet of code and it works great:
https://discourse.nativescript.org/t/how-to-detect-if-component-is-in-screen-view-is-visible/1148/4
I can change the background colour of the "StackLayout" inside the "ng-template".
But I can't access to my custom component variables to modify his behaviour.
For example, if "my-custom-component" is shown, I want to change the property "isShown" in the "card" object passed in the "aCard" attribute.
Thanks to all :)
EDIT1:
"isShown" is a custom variable that I have used for this test. My idea is to calculate in the afterScroll function what is the cards visible and pass to aCard the parameter to change his behaviour.
You could find the location of each child component inside ScrollView upon scroll event, comparing the same with the vertical offset will let you know whether the component is really visible on screen.
Here is a Playground example. As you scroll down / up, the background color of visible components will turn green, red otherwise.
onScroll(event: EventData) {
const scrollView = <ScrollView>event.object,
verticalOffset = scrollView.verticalOffset,
height = scrollView.getActualSize().height,
visibleRange = verticalOffset + height,
container = <StackLayout>this.container.nativeElement;
let index = 0;
container.eachLayoutChild((childView) => {
const locationY = childView.getLocationRelativeTo(container).y;
this.cards[index].isShown = locationY >= verticalOffset && locationY <= visibleRange
index += 1;
});
}
You need to update your allList object as NgForOf is bindable, it will update the card and that will reflect in [acard] of your my-custom-component
In the scroll event where you must be playing with relative height you take a unique variable to identify the component that is shown and change the property for that index in allList.
I have created a sample playgrod here where I am changing the text of the custom component Label to isShown if scroll height is greater than 300. The way I am changing the label name, you can have a boolean variable in allList in change that where you have your logic to change the background color of stackLayout. Let me know if you want to update the playgrond.

Dynamically size Nativescript ContentView within ScrollView based on device height

I have a Nativescript Vue component template defined as:
<ScrollView>
<StackLayout>
<ContentView ref="mapContainer" height="500" width="100%">
<Mapbox
accessToken="my access key"
mapStyle="outdoors-v9"
hideCompass="true"
zoomLevel="10.2" ,
latitude="51.949266"
longitude="-12.183571"
showUserLocation="true"
disableZoom="true"
disableRotation="true"
disableScroll="true"
disableTilt="true"
#mapReady="onMapReady($event)">
</Mapbox>
</ContentView>
<Label text="A test label"/>
</StackLayout>
</ScrollView>
When the component is mounted, I want to set the height of mapContainer to roughly 75% of the screen height. To do so, I have:
export default {
name: "MyPage",
mounted () {
this.$refs.mapContainer.height = platform.screen.mainScreen.heightDIPs
}
...
}
But this does nothing, and the ContentView remains at 500dp tall.
height is meant to be a setter, but I figure I'm missing a redraw (?) to get this change to take effect, but not sure how?
In order to access the ContentView via refs, you must use .nativeView property.
this.$refs.mapContainer.nativeView.height = platform.screen.mainScreen.heightDIPs
Also you don't have to calculate the height but simply set the height in percentage (70%) instead of fixed value (500) Or use a GridLayout with 7 partition (7*).

NativeScript Angular 2 Get more data when Scroll to Bottom (endless scrolling)

HI for example below is my view
<ScrollView>
<StackLayout>
<StackLayout *ngFor="let kid of kids" id="kidList">
<StackLayout orientation="horizontal" class="some-class">
<Lable text="{{ kid.fname }} {{ kid.lname }}"></Lable>
<Lable text="{{ kid.age }} years ago"></Lable>
</StackLayout>
</StackLayout>
</StackLayout>
</ScrollView>
I want to append the more data getting from server to 'kidList' when scroll reaches to bottom of screen in {N} aAngular2.
It's very hard to me build the layouts and adding childs in 'js' like below(KidInformation has more data).
let stackLayout = new StackLayout();
stackLayout.addChild('something newly constructed with JS')
Is there a way we can do in My Component just by adding child as view by passing local parameters to view , I mean like in below way
let kidListStacklayout = view.getViewById(this.page, 'kidList');
kidListStacklayout.addChild('views/kid/kid-item.html')
and kid-item.html will look like
<StackLayout orientation="horizontal" class="some-class">
<Lable text="{{ kid.fname }} {{ kid.lname }}"></Lable>
<Lable text="{{ kid.age }} years ago"></Lable>
</StackLayout>
The stock list view supports what you want. Don't use a scrollview and adding more layouts to the screen. This will cause lag and other issues. A listview recycles UI view components to reduce overhead of the layout growing in size. You want to use the loadMore event on the list view. https://docs.nativescript.org/cookbook/ui/list-view
Of course as the comment above ^^^ the free UI suite from telerik provides the RadListView which also supports infinite scrolling with a loadMore event.
Find out how to do it ( using Angular & TypeScript) :
import { ScrollView, ScrollEventData } from "ui/scroll-view";
#Component({...})
class ComponentClass implements OnInit, AfterViewInit {
#ViewChild("scrollid") scrollView: ElementRef;
ngOnInit(){
let scrollv : ScrollView = <ScrollView>this.scrollView.nativeElement;
scrollv.on(ScrollView.scrollEvent, function (args: ScrollEventData) {
if(scrollv.scrollableHeight === args.scrollY){
console.log("load more items here !!! ");
}
});
}
}
The scrollv.scrollableHeight gets updated by itself.
Tested on android emulator only. Must work on both Plateforms.

Resources