Nativescript: GridLayout with full-height map and auto-height ListView: Android shrinks the ListView

Created on 13 Mar 2017  Â·  5Comments  Â·  Source: NativeScript/NativeScript

Hi,

This is very interesting problem that I cannot easily explain, therefore I'm also providing screenshots.

I have a GridLayout with 2 rows. First row wraps a GoogleMaps view which is full-height (*). Second row wraps a ListView with auto-height (auto), which wraps a couple of buttons. The problem is that Android shows only the first button:
http://3.1m.yt/yhAjkBp.png

Here is my layout:

<Page xmlns="http://www.nativescript.org/tns.xsd" xmlns:maps="nativescript-google-maps-sdk" loaded="onload">
   <GridLayout rows="*, auto">
      <maps:mapView row="0" latitude="{{ latitude }}" longitude="{{ longitude }}" zoom="{{ zoom }}" bearing="{{ bearing }}" tilt="{{ tilt }}" padding="{{ padding }}" mapReady="onMapReady" markerSelect="onMarkerSelect" markerInfoWindowTapped="onMarkerInfoWindowTap" cameraChanged="onCameraChange" />
      <ListView items="{{ buttons }}" loaded="listViewLoaded" row="1">
         <ListView.itemTemplate>
            <GridLayout rows="auto">
               <Button id="{{id}}" text="{{name}}" mode="{{mode}}" class="{{ type }}" tap="onButtonTap" row="0"/>
            </GridLayout>
         </ListView.itemTemplate>
      </ListView>
   </GridLayout>
</Page>

TNS version 2.5.0
TNS Core Modules version 2.5.0

android question

Most helpful comment

Hi @ickata
In my further investigation, I found that this is something expected while setting auto-height (auto) to the ListView inside a GridLayout.
This has been related to the fact that to the ListView we should set correct height, which allows us to setup the height for the items.

However, for your case, you could use Repeater component, which will allow you to create a custom template to bind to it array with the needed content. While using this component the height will be changed will be changed according to the items number. For Example:

XML

<Page xmlns="http://schemas.nativescript.org/tns.xsd" xmlns:maps="nativescript-google-maps-sdk"  navigatingTo="navigatingTo">
                  <GridLayout rows="* auto">
                  <GridLayout row="0" >
                      <maps:mapView latitude="{{ latitude }}" longitude="{{ longitude }}" 
                        zoom="{{ zoom }}" bearing="{{ bearing }}" 
                        tilt="{{ tilt }}" padding="{{ padding }}" mapReady="OnMapReady"  
                      markerSelect="onMarkerSelect" 
                      cameraChanged="onCameraChanged" />
                  </GridLayout>

                      <Repeater row="1" items="{{ source }}">
                          <Repeater.itemsLayout>
                              <StackLayout />
                          </Repeater.itemsLayout>
                          <Repeater.itemTemplate>
                              <Label text="{{title}}" textWrap="true" />
                          </Repeater.itemTemplate>
                      </Repeater>

                  </GridLayout>

</Page>

TypeScript

import { EventData, Observable} from "data/observable";
import {ObservableArray} from "data/observable-array";
import { Page } from "ui/page";
var mapsModule = require("nativescript-google-maps-sdk");

// Event handler for Page "navigatingTo" event attached in main-page.xml

export function navigatingTo(args: EventData) {
    // Get the event sender
    var page = <Page>args.object;
    var array = new ObservableArray();
    var listviewarray=[{title:"title1"}, {title:"title2"}, {title:"title3"}, {title:"title4"}, {title:"title4"}, {title:"title4"}]

    array.push({
        title: '1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
        image: 'http://lorempixel.com/200/100/'
    });
    array.push({
        title: '1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
        image: 'http://lorempixel.com/200/100/'
    });
    array.push({
        title: '1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
        image: 'http://lorempixel.com/200/100/'
    });

    var observable = new Observable({source: array});
    observable.set("latitude", -33.86);
        observable.set("longitude", 151.20);
        observable.set("zoom", 8);
        observable.set("bearing", 0);
        observable.set("tilt", 0);
        observable.set("padding", [40, 40, 40, 40]);


    observable.set("source", listviewarray);
    page.bindingContext = observable;
}

export function OnMapReady(args) {
  var mapView = args.object;

  console.log("Setting a marker...");
  var marker = new mapsModule.Marker();
  marker.position = mapsModule.Position.positionFromLatLng(-33.86, 151.20);
  marker.title = "Sydney";
  marker.snippet = "Australia";
  marker.userData = { index : 1};
  mapView.addMarker(marker);
}

export function onMarkerSelect(args) {
   console.log("Clicked on " +args.marker.title);
}

export function onCameraChanged(args) {
    console.log("Camera changed: " + JSON.stringify(args.camera)); 
}

Hope this helps

All 5 comments

Hi @ickata,
Setting auto-height (auto) for the ListView, in Android we will set the minimum needed height, which is needed to display one item.

For your case, you could try to set full-height (*) for both components, which should allow you to show the full content of the ListView.
Another option is to setup height property of the ListView. For Example:

<ListView items="{{ source }}" height="200"  loaded="onLoaded" itemLoading="onItemLoading" itemTap="onItemTap">
              .....
          </ListView>

Let me know, whether this helps or if I could assist you further.

Hi @tsonevn ,

Thank you for the really fast reply!

The first workaround that you suggested won't work for me, unfortunately. I wish a layout where my map is always full-height and the ListView takes the rest of the available space, but enough to show all of its items. My list items are dynamic and change based on certain conditions. Therefore, I need the ListView to be "auto", and the map takes the rest of the space. In case of zero items map is 100% high.

The dynamic list items make your second proposal also invalid. I don't know how many items I will have at a time, therefore I cannot set fixed height.

I came to a workaround. Instead of using the ListView, I can use a GridLayout and dynamically append/remove items via gridlayout.addChild and gridlayout.removeChild. Here is the new structure:

<Page xmlns="http://www.nativescript.org/tns.xsd" xmlns:maps="nativescript-google-maps-sdk" loaded="onload">
   <GridLayout rows="*, auto">
      <maps:mapView row="0" latitude="{{ latitude }}" longitude="{{ longitude }}" zoom="{{ zoom }}" bearing="{{ bearing }}" tilt="{{ tilt }}" padding="{{ padding }}" mapReady="onMapReady" markerSelect="onMarkerSelect" markerInfoWindowTapped="onMarkerInfoWindowTap" cameraChanged="onCameraChange" />
      <GridLayout id="buttons" rows="auto, auto, auto, auto, auto, auto, auto, auto, auto, auto, auto, auto" row="1">
         <Button text="Button 1" tap="onButtonTap" row="0"/>
         <Button text="Button 2" tap="onButtonTap" row="1"/>
         <Button text="Button 3" tap="onButtonTap" row="2"/>
      </GridLayout>
   </GridLayout>
</Page>

Unfortunately the solution is not very elegant and has 2 drawbacks:

  1. I must specify a very long comma-separated list in GridLayout's "rows" attribute. The reason is that I don't know how many items I will have at a time. The good news is that I can set 20+ "auto"s and this won't break my layout even if I have just a single item.

  2. I can't use much from the framework via the ObservableArray and the ListView – I have to manipulate my views on my own.

While 2 is kinda OK, 1 does not look elegant. But it is working (I created a very quick prototype to prove it) and is going to fulfill my requirements.

I will let you decide whether to close this ticket or leave it open. I have a workaround, but in my opinion this inconsistency between the two platforms (iOS and Android) must be somehow equalized by NativeScript.

Once again, thanks for the fast reply and the great TNS!

Hi @ickata
In my further investigation, I found that this is something expected while setting auto-height (auto) to the ListView inside a GridLayout.
This has been related to the fact that to the ListView we should set correct height, which allows us to setup the height for the items.

However, for your case, you could use Repeater component, which will allow you to create a custom template to bind to it array with the needed content. While using this component the height will be changed will be changed according to the items number. For Example:

XML

<Page xmlns="http://schemas.nativescript.org/tns.xsd" xmlns:maps="nativescript-google-maps-sdk"  navigatingTo="navigatingTo">
                  <GridLayout rows="* auto">
                  <GridLayout row="0" >
                      <maps:mapView latitude="{{ latitude }}" longitude="{{ longitude }}" 
                        zoom="{{ zoom }}" bearing="{{ bearing }}" 
                        tilt="{{ tilt }}" padding="{{ padding }}" mapReady="OnMapReady"  
                      markerSelect="onMarkerSelect" 
                      cameraChanged="onCameraChanged" />
                  </GridLayout>

                      <Repeater row="1" items="{{ source }}">
                          <Repeater.itemsLayout>
                              <StackLayout />
                          </Repeater.itemsLayout>
                          <Repeater.itemTemplate>
                              <Label text="{{title}}" textWrap="true" />
                          </Repeater.itemTemplate>
                      </Repeater>

                  </GridLayout>

</Page>

TypeScript

import { EventData, Observable} from "data/observable";
import {ObservableArray} from "data/observable-array";
import { Page } from "ui/page";
var mapsModule = require("nativescript-google-maps-sdk");

// Event handler for Page "navigatingTo" event attached in main-page.xml

export function navigatingTo(args: EventData) {
    // Get the event sender
    var page = <Page>args.object;
    var array = new ObservableArray();
    var listviewarray=[{title:"title1"}, {title:"title2"}, {title:"title3"}, {title:"title4"}, {title:"title4"}, {title:"title4"}]

    array.push({
        title: '1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
        image: 'http://lorempixel.com/200/100/'
    });
    array.push({
        title: '1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
        image: 'http://lorempixel.com/200/100/'
    });
    array.push({
        title: '1',
        description: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
        image: 'http://lorempixel.com/200/100/'
    });

    var observable = new Observable({source: array});
    observable.set("latitude", -33.86);
        observable.set("longitude", 151.20);
        observable.set("zoom", 8);
        observable.set("bearing", 0);
        observable.set("tilt", 0);
        observable.set("padding", [40, 40, 40, 40]);


    observable.set("source", listviewarray);
    page.bindingContext = observable;
}

export function OnMapReady(args) {
  var mapView = args.object;

  console.log("Setting a marker...");
  var marker = new mapsModule.Marker();
  marker.position = mapsModule.Position.positionFromLatLng(-33.86, 151.20);
  marker.title = "Sydney";
  marker.snippet = "Australia";
  marker.userData = { index : 1};
  mapView.addMarker(marker);
}

export function onMarkerSelect(args) {
   console.log("Clicked on " +args.marker.title);
}

export function onCameraChanged(args) {
    console.log("Camera changed: " + JSON.stringify(args.camera)); 
}

Hope this helps

@tsonevn , thank you very much for the help! I've never used Repeater component before – thought that the only way to loop ObservableArray is via the ListItem (even though I searched for alternative).

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

Was this page helpful?
0 / 5 - 0 ratings