44

I have a RecyclerView which is a vertical scrolling list of items. Each list item contains a Google Maps V2 MapView in Lite Mode. I'm taking advantage of this new feature which returns bitmaps instead of a full-blown map as a replacement to the Google Static Maps API.

MapView requires that you call onCreate(), onResume(), onPause(), onDestroy() etc. from the parent Activity/Fragment's corresponding method. Where is the proper place to call these from the RecyclerView.Adapter and/or RecyclerView.ViewHolder?

How can I clean up recycled MapViews so that memory doesn't leak, while keeping the list jank free?

Google says Lite Mode can be used in lists:

... ‘lite mode’ map option, ideal for situations where you want to provide a number of smaller maps, or a map that is so small that meaningful interaction is impractical, such as a thumbnail in a list.

ListItem.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent">

    <com.google.android.gms.maps.MapView
        android:id="@+id/mapImageView"
        xmlns:map="http://schemas.android.com/apk/res-auto"
        android:layout_width="80dp"
        android:layout_height="100dp"
        map:liteMode="true"
        map:mapType="normal"
        map:cameraZoom="15"/>

<!-- ... -->

</RelativeLayout>

RecyclerView.Adapter and ViewHolder

public class NearbyStopsAdapter extends RecyclerView.Adapter<NearbyStopsAdapter.ViewHolder> {

    private final Context mContext;

    public class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        MapView map;

        public ViewHolder(View view) {
            super(view);
            map = (MapView) view.findViewById(R.id.mapImageView);
            // Should this be created here?
            map.onCreate(null);
            map.onResume();
        }
    }

    public NearbyStopsAdapter(Context c) {
        this.mContext = c;
    }

    @Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
        View itemView = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item_nearby_stop, viewGroup, false);
        return new ViewHolder(itemView);
    }

    @Override public void onBindViewHolder(ViewHolder holder, int position) {
        //Call Async Map here?
        holder.map.getMapAsync(this);
    }

    @Override public void onViewRecycled(ViewHolder holder) {
        // Cleanup MapView here?
//        if (holder.map != null) {
//            holder.map.onPause();
//            holder.map.onDestroy();
//        }
    }

    @Override public void onViewAttachedToWindow(ViewHolder holder) {
        // Setup MapView here?
//            holder.map.onCreate(null);
//            holder.map.onResume();
    }

    @Override public void onViewDetachedFromWindow(ViewHolder holder) {
        // Cleanup MapView here?
//        if (holder.map != null) {
//            holder.map.onPause();
//            holder.map.onDestroy();
//        }
    }

    // ...
}

Logcat:

I/Google Maps Android API﹕ Google Play services package version: 659943
W/Google Maps Android API﹕ Map Loaded callback is not supported in Lite Mode
W/Google Maps Android API﹕ Buildings are not supported in Lite Mode
W/Google Maps Android API﹕ Indoor is not supported in Lite Mode
W/Google Maps Android API﹕ Toggling gestures is not supported in Lite Mode
W/Google Maps Android API﹕ Toggling gestures is not supported in Lite Mode
W/Google Maps Android API﹕ Toggling gestures is not supported in Lite Mode
W/Google Maps Android API﹕ Toggling gestures is not supported in Lite Mode

Update: (Jun 8, 2018) Google has released a code sample for using Lite Maps in a ListView. See here

Ryan R
  • 8,342
  • 15
  • 84
  • 111
  • Is this question about your RecyclerView not behaving smooth or garbage collection of a MapView? I don't think these two problems are tightly connected. – Michał Klimczak Feb 25 '15 at 08:48
  • The jank occurs when calling `getMapAsync(this)` in `onBindViewHolder(...)`, not necessarily on a GC. Though, there will be jank there too. – Ryan R Mar 02 '15 at 04:05
  • Moving mapView.onCreate(null); and mapView.getMapAsync(this); from onBindViewHolder to the ViewHolder constructor definitely reduced a bunch of jank for me. – scottyab Aug 26 '16 at 14:09
  • 1
    the link you provided has expired, here's the new one, definitely useful and should solve the probelm: https://github.com/googlemaps/android-samples/blob/master/ApiDemos/java/app/src/main/java/com/example/mapdemo/LiteListDemoActivity.java – dumbfingers Jun 08 '18 at 10:27

6 Answers6

29

Solution as following:

  1. Implement OnMapReadyCallback in ViewHolder class.
  2. In onMapReady, call MapsInitializer.initialize, to gaurantee features can to be used before obtaining a map.

Use this class to initialize the Google Maps Android API if features need to be used before obtaining a map. It must be called because some classes such as BitmapDescriptorFactory and CameraUpdateFactory need to be initialized.

  1. Recycle map from onViewRecycled.


    public class NearbyStopsAdapter extends RecyclerView.Adapter<NearbyStopsAdapter.ViewHolder> {


       @Override 
       public void onBindViewHolder(ViewHolder holder, int position)  
       {
          //get 'location' by 'position' from data list
          //get GoogleMap
          GoogleMap thisMap = holder.gMap;
          //then move map to 'location'
          if(thisMap != null) 
             //move map to the 'location' 
             thisMap.moveCamera(...);          
       }


       //Recycling GoogleMap for list item
       @Override 
       public void onViewRecycled(ViewHolder holder) 
       {
          // Cleanup MapView here?
          if (holder.gMap != null) 
          {
              holder.gMap.clear();
              holder.gMap.setMapType(GoogleMap.MAP_TYPE_NONE);
          }
       }



       public class ViewHolder extends RecyclerView.ViewHolder implements OnMapReadyCallback { 

           GoogleMap gMap; 
           MapView map;
            ... ... 

           public ViewHolder(View view) {
              super(view);
              map = (MapView) view.findViewById(R.id.mapImageView);

              if (map != null) 
              {
                 map.onCreate(null);
                 map.onResume();
                 map.getMapAsync(this);
              }

          }


          @Override
          public void onMapReady(GoogleMap googleMap) {
              //initialize the Google Maps Android API if features need to be used before obtaining a map 
              MapsInitializer.initialize(getApplicationContext());
              gMap = googleMap;

              //you can move map here to item specific 'location'
              int pos = getPosition();
              //get 'location' by 'pos' from data list  
              //then move to 'location'
              gMap.moveCamera(...);

                  ... ...
         }

       }
    } 
KVISH
  • 12,923
  • 17
  • 86
  • 162
Xcihnegn
  • 11,579
  • 10
  • 33
  • 33
  • Yes this actually makes it very snappy, thought unfortunately there is no guarantee that the map is ready before `onBindViewHolder(...)` is called, and if it's not ready then any map properties you set will be applied when `getMapAsync(this)` catches up. However, if `getMapAsync(this)` is called before `onBindViewHolder(...)` is called, then any changes you want to perform on the map like moving the camera or placing a marker needs to be performed again on the map causing jank on the list. – Ryan R Feb 25 '15 at 05:17
  • `MapsInitializer.initialize(getApplicationContext());` seems can solve it, look my modified answer. – Xcihnegn Feb 25 '15 at 09:27
  • @AniV How is going on your issue? still problem? please tell – Xcihnegn Feb 27 '15 at 07:46
  • The map must be initialized before using it yes. However, the data to move the map for example may not be ready as `getMapAsync(this)` may have returned before `onBindViewHolder(...)` is called. – Ryan R Feb 27 '15 at 22:25
  • `MapsInitializer.initialize`, which garantes map features can to be used even before obtaining a map. – Xcihnegn Feb 28 '15 at 09:57
  • Yes, `onMapReady(...)` may be called, though the data used to manipulate the camera is not ready since, `onBindViewHolder(...)` may not have been called yet. – Ryan R Mar 02 '15 at 04:04
  • How about your actual case, the solution is working or not? or just your worry about? – Xcihnegn Mar 02 '15 at 14:54
  • I have tested this on a device. This method would work if there was some way to ensure that `onBindViewHolder(...) ` is called before `getMapAsync(this)` completes. – Ryan R Mar 02 '15 at 16:11
  • Have you got the case `onMapReady` called before `onBindViewHolder(...)` be called? and if so what wrong happen? – Xcihnegn Mar 02 '15 at 16:19
  • Yes. For example, in my on `onBindViewHolder(...)` I would like the map to move to a specific location. However when `onMapReady(...)` returns before `onBindViewHolder(...)`, theres is no destination location set and the map shows its default location at (0.0, 0.0) or to whatever you've predefined in `XML`. – Ryan R Mar 02 '15 at 16:28
  • `onBindViewHolder Called by RecyclerView to display the data at the specified position`, so that is right place to move map to specific location where based the item `position`, is it quite logical? – Xcihnegn Mar 02 '15 at 16:51
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/72076/discussion-between-ryan-r-and-xcihnegn). – Ryan R Mar 02 '15 at 17:07
  • I just modified my answer with `onBindViewHolder` and map recycling – Xcihnegn Mar 02 '15 at 18:54
  • `MapView` is not the same as `GoogleMap`. You cannot do `map = googleMap;` in `onMapReady(...)` – Ryan R Mar 02 '15 at 19:51
  • This is getting closer, though there is the case when the list is first loaded `holder.gMap` can be null in `onBindViewHolder(...)`. The user would have to move the list to "refresh" the item. – Ryan R Mar 03 '15 at 17:32
  • 1
    For this case, if you do not want null, then in your XML, the `MapView` should have a dumy img/bmp , perhaps this is only solution because we can not control the process. – Xcihnegn Mar 03 '15 at 17:39
  • 2
    Here is my adapter, it's works pretty nice, maybe it would be usefull for you:https://gist.github.com/Yazon2006/5d7fd1716093441e3b25e088f9332cf8 – Yazon2006 Jun 16 '16 at 08:33
  • From Google API for Android on [MapsInitializer reference](https://developers.google.com/android/reference/com/google/android/gms/maps/MapsInitializer): If you are using MapFragment or MapView and have already obtained a (non-null) GoogleMap by calling getMapAsync() on either of these classes and waiting for the onMapReady(GoogleMap map) callback, then you do not need to worry about this class. – THZ Jun 06 '17 at 11:22
  • googlemap v1 will be remove soon, as you can see when publish app to store then u will get this message – famfamfam Dec 29 '21 at 06:57
5

Google says:

When using the API in fully interactive mode, users of the MapView class must forward all the activity life cycle methods to the corresponding methods in the MapView class. Examples of the life cycle methods include onCreate(), onDestroy(), onResume(), and onPause().

When using the MapView class in lite mode, forwarding lifecycle events is optional, except for the following situations:

It is mandatory to call onCreate(), otherwise no map will appear. If you wish to show the My Location dot on your lite mode map and use the default location source, you will need to call onResume() and onPause(), because the location source will only update between these calls. If you use your own location source, it's not necessary to call these two methods.

So on lite mode you don't have to worry about onDestroy(), onResume() and onPause()

Community
  • 1
  • 1
BartoszJarocki
  • 106
  • 1
  • 3
3

Find Google Maps Android API Lite Mode Example (source code)

Google Map Lite - github.com

Jon
  • 9,156
  • 9
  • 56
  • 73
Chathura Wijesinghe
  • 3,310
  • 3
  • 25
  • 41
  • googlemap v1 will be remove soon, as you can see when publish app to store then u will get this message – famfamfam Dec 29 '21 at 06:57
3

Google map provides Lite Mode

The Maps SDK for Android can serve a bitmap image of a map, offering limited interactivity to the user. This is called a lite mode map.

Lite Mode

Follow the LiteListDemoActivity: Displaying maps efficiently in ListViews using lite mode example.

Kasim Rangwala
  • 1,765
  • 2
  • 23
  • 44
0

You need to have a separate View Holder class. The RecyclerView Adapter class will just have onCreateViewHolder() and onBindViewHolder().

Your Layout file should look something similar to this:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MyActivity">

    <view
    <com.google.android.gms.maps.MapView
    android:id="@+id/mapImageView"
    xmlns:map="http://schemas.android.com/apk/res-auto"
    android:layout_width="80dp"
    android:layout_height="100dp"
    map:liteMode="true"
    map:mapType="normal"
    map:cameraZoom="15" />

</RelativeLayout>

And the onCreate(), onDestroy() will be called in the Activity class as usual.

Please follow this tutorial to get a complete overview.

AniV
  • 3,997
  • 1
  • 12
  • 17
  • 1
    I think you might have missed the question. There is already a ViewHolder defined in the `RecyclerView.Adapter`. The question is where should `MapView.onCreate()`, `MapView.onDestroy()` etc. be called? – Ryan R Feb 19 '15 at 20:05
  • Please see the line just below the layout file code "And the onCreate(), onDestroy() will be called in the Activity class as usual." – AniV Feb 19 '15 at 20:56
  • Your link does not include a `MapView`. How is it relevant? – Ryan R Feb 19 '15 at 23:11
  • It is the usual Activity class... With onCreate() and rest of Android lifecycles methods described. For more details see this code that has mapView in Franment and data pulled from layout inflater https://gist.github.com/joshdholtz/4522551 – AniV Feb 19 '15 at 23:18
  • 1
    The `RecyclerView` does not have `onCreate()` and the rest of the Android lifecycle methods. And the `RecyclerView` contains many list items which each contain a `MapView`, which is recycled when the user scrolls the list. As a result the `MapView` for each item has to be created and destroyed. The question is where to call the `MapView.onCreate()`, etc. from within the `RecyclerView` not the `Fragment` or `Activity`. – Ryan R Feb 19 '15 at 23:29
0

I have removed this Override method because every time this gives empty map when testing and it works perfectly in my recyclerView.

@Override 
public void onViewRecycled(ViewHolder holder) 
{
  // Cleanup MapView here?
  if (holder.gMap != null) 
  {
      holder.gMap.clear();
      holder.gMap.setMapType(GoogleMap.MAP_TYPE_NONE);
  }
}

You can try it if above code does not work in your case as well.

Nathan Tuggy
  • 2,237
  • 27
  • 30
  • 38