Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Google Maps v2 MapFragment is extremely laggy on returning from the backstack

I'm working on an app that makes use of a v2 MapFragment, and I'm running into very strange behavior. I've created a subclass of MapFragment to handle some custom behavior (handling Markers, tweaking the menu options, etc.), and on first load it all works beautifully. I then embed a new fragment into my activity, pushing the custom MapFragment onto the backstack. When I return the map from the backstack, though, things get weird; panning the map becomes extremely laggy (we're talking ~1 FPS), both for manual dragging/zooming and for animations caused by clicking on pins. And then, if I interact with any part of the overflow menu, even just opening it and dismissing it again, the lag immediately clears up. Nothing else seems to fix it (short of closing/reopening the app); interacting with the non-overflow menu items and the navigation drawer does nothing to help. I've never seen anything like this, nor can I find anyone who's described a similar problem before. Any ideas, suggestions, and/or fixes would be welcome.

To answer a few questions before they get asked:

  • Yes, I'm calling the super versions of all the lifecycle methods I override (onCreate(), onCreateView() [I'm also returning what the super returns for that one], and onDestroyView()).
  • As far as I can tell, I'm cleaning up the map properly. Every time I refresh the pins, I'm calling remove() on each of them and then clean() on the map itself, and I do all that in onDestroyView() as well.

And lastly, for reference, this is the code that adds the new fragment:

getFragmentManager().beginTransaction().replace(R.id.main_content_container, new JoinGroupFragment()).addToBackStack(null).commit();

And when I'm done with it, I just call:

getFragmentManager().popBackStack();

EDIT: I'm not sure how much help it'll be, but here's the custom MapFragment:

public class CustomMapFragment extends MapFragment {

    private static final String DIALOG_TAG = "CUSTOM_MAP_FRAGMENT_DIALOG";
    private static final int DEFAULT_ZOOM = 14;
    private static final int MARKER_ZOOM = 15;
    private static final int DEFAULT_PADDING = 80;
    private static final int ORANGE_THRESHOLD_MINUTES = 7;
    private static final int BLUE_THRESHOLD_MINUTES = 20;

    public static final String KEY_GROUP_NAME = "GROUP_NAME";
    public static final String KEY_GROUP_ID = "GROUP_ID";

    private TextView mGroupNameOverlay;
    private GoogleMap mMap;
    private ArrayList<Marker> mMarkers;
    private Marker mSelectedMarker;
    private ArrayList<Group> mAllGroups;
    private Group mCurrentGroup;
    private ArrayList<Location> mAllLocations;
    private boolean mMapReady;
    private String mUsername;
    private boolean mCenterOnUser;

    public CustomMapFragment() {

    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setHasOptionsMenu(true);

        mMarkers = new ArrayList<>();
        mAllLocations = new ArrayList<>();

        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
        mUsername = prefs.getString(PreferenceUtils.KEY_USERNAME, null);
        mCenterOnUser = prefs.getBoolean(PreferenceUtils.KEY_CENTER_ON_ME, false);
        mSelectedMarker = null;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        ViewGroup view = (ViewGroup)super.onCreateView(inflater, container, savedInstanceState);

        if (view != null) {
            // View should never be null; MapFragments have a FrameLayout as their top level parent
            mGroupNameOverlay = (TextView)inflater.inflate(R.layout.group_name_overlay, view, false);
            view.addView(mGroupNameOverlay);
        }

        Bundle results = ((MainActivity)getActivity()).getFragmentResults();
        if (results != null) {
            String name = results.getString(KEY_GROUP_NAME);
            String id = results.getString(KEY_GROUP_ID);
            if (!StringUtils.isNullOrEmpty(name) && !StringUtils.isNullOrEmpty(id)) {
                mCurrentGroup = new Group(name, id);
                mAllGroups.add(mCurrentGroup);
            }
        }

        if (mCurrentGroup != null) {
            updateGroupNameOverlay(mCurrentGroup.getGroupName());
        }

        getMapAsync(new OnMapReadyCallback() {
            @Override
            public void onMapReady(GoogleMap googleMap) {
                mMap = googleMap;
                mMap.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() {
                    @Override
                    public boolean onMarkerClick(Marker marker) {
                        mSelectedMarker = marker;
                        getActivity().invalidateOptionsMenu();
                        return false;
                    }
                });
                mMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() {
                    @Override
                    public void onMapClick(LatLng latLng) {
                        mSelectedMarker = null;
                        getActivity().invalidateOptionsMenu();
                    }
                });
                populateMap(true, false);
            }
        });

        GetGroupsRequest request = new GetGroupsRequest();
        request.setListener(new GetGroupsRequestListener());
        RequestProcessor.getInstance(getActivity()).queueRequest(request);

        return view;
    }

    @Override
    public void onDestroyView() {
        mSelectedMarker = null;
        for (Marker marker : mMarkers) {
            marker.remove();
        }
        mMarkers.clear();
        mMap.clear();
        mMap = null;
        mMapReady = false;
        super.onDestroyView();
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        if (mSelectedMarker == null) {
            inflater.inflate(R.menu.menu_map, menu);
        }
        else {
            inflater.inflate(R.menu.menu_marker, menu);
        }
        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.map_menu_refresh_pins:
                performLocationsRequest(false);
                return true;
            case R.id.map_menu_recenter_zoom:
                populateMap(true, true);
                return true;
            case R.id.map_menu_select_group:
                DialogFragment selectDialog = new DialogFragment() {

                    @Override
                    public Dialog onCreateDialog(Bundle savedInstanceState) {
                        String[] groups = new String[mAllGroups.size()];
                        for (int i = 0; i < groups.length; i++) {
                            groups[i] = mAllGroups.get(i).getGroupName();
                        }
                        return new AlertDialog.Builder(getActivity())
                                .setItems(groups, new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialog, int which) {
                                        if (!mAllGroups.get(which).equals(mCurrentGroup)) {
                                            mCurrentGroup = mAllGroups.get(which);
                                            updateGroupNameOverlay(mCurrentGroup.getGroupName());
                                            performLocationsRequest(true);
                                        }
                                    }
                                })
                                .create();
                    }
                };
                selectDialog.show(getFragmentManager(), DIALOG_TAG);
                return true;
            case R.id.map_menu_join_group:
                getFragmentManager().beginTransaction().replace(R.id.main_content_container, new JoinGroupFragment()).addToBackStack(null).commit();
                return true;
            case R.id.map_menu_create_group:
                CreateDialogFragment createDialog = new CreateDialogFragment();
                createDialog.show(getFragmentManager(), DIALOG_TAG);
                return true;
            case R.id.map_marker_zoom:
                if (mSelectedMarker != null) {
                    mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(mSelectedMarker.getPosition(), MARKER_ZOOM));
                }
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    private void performLocationsRequest(boolean autoZoom) {
        GetLocationsRequest request = new GetLocationsRequest(mCurrentGroup.getGroupId());
        request.setListener(new GetLocationsRequestListener(autoZoom));
        RequestProcessor.getInstance(getActivity()).queueRequest(request);
    }

    private void updateGroupNameOverlay(final String groupName) {
        if (mGroupNameOverlay != null) {
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    if (groupName == null) {
                        mGroupNameOverlay.setText(R.string.map_group_overlay_no_group);
                    }
                    else {
                        mGroupNameOverlay.setText(getString(R.string.map_group_overlay_group, groupName));
                    }
                }
            });
        }
    }

    private void populateMap(boolean zoom, boolean animate) {
        if (!mMapReady) {
            mMapReady = true;
        }
        else {
            CameraUpdate update = null;
            mSelectedMarker = null;
            for (Marker marker : mMarkers) {
                marker.remove();
            }
            mMarkers.clear();
            mMap.clear();
            if (mAllLocations.size() == 1) {
                Location location = mAllLocations.get(0);
                mMarkers.add(addMarker(location));
                update = CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(), location.getLongitude()), DEFAULT_ZOOM);
            }
            else if (mAllLocations.size() > 1) {
                LatLngBounds.Builder builder = new LatLngBounds.Builder();
                for (Location location : mAllLocations) {
                    mMarkers.add(addMarker(location));
                    if (mCenterOnUser) {
                        if (location.getUsername().equals(mUsername)) {
                            update = CameraUpdateFactory.newLatLngZoom(new LatLng(location.getLatitude(), location.getLongitude()), DEFAULT_ZOOM);
                        }
                    }
                    else {
                        builder.include(new LatLng(location.getLatitude(), location.getLongitude()));
                    }
                }
                if (!mCenterOnUser) {
                    update = CameraUpdateFactory.newLatLngBounds(builder.build(), DEFAULT_PADDING);
                }
            }

            if (update != null && zoom) {
                if (animate) {
                    mMap.animateCamera(update);
                }
                else {
                    mMap.moveCamera(update);
                }
            }
        }
    }

    private Marker addMarker(Location location) {
        String timestamp;
        long minutesOld = (new Date().getTime() - location.getLastReported()) / 60000;
        float hue = BitmapDescriptorFactory.HUE_RED;
        if (minutesOld < 1) {
            timestamp = getString(R.string.map_timestamp_just_now);
        }
        else if (minutesOld < 2) {
            timestamp = getString(R.string.map_timestamp_one_minute);
        }
        else {
            timestamp = getString(R.string.map_timestamp_n_minutes, minutesOld);
            if (minutesOld >= ORANGE_THRESHOLD_MINUTES) {
                hue = BitmapDescriptorFactory.HUE_ORANGE;
            }
            if (minutesOld >= BLUE_THRESHOLD_MINUTES) {
                hue = BitmapDescriptorFactory.HUE_BLUE;
            }
        }
        LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
        return mMap.addMarker(new MarkerOptions()
                .position(latLng)
                .icon(BitmapDescriptorFactory.defaultMarker(hue))
                .title(location.getUsername())
                .snippet(timestamp));
    }

    private class GetGroupsRequestListener extends RequestListener<GetGroupsResponse> {

        public GetGroupsRequestListener() {
            super(getActivity());
        }

        @Override
        protected void onRequestComplete(GetGroupsResponse response) {
            mAllGroups = response.getGroups();
            if (mAllGroups.size() > 0) {
                if (mCurrentGroup == null) {
                    mCurrentGroup = mAllGroups.get(0);
                    updateGroupNameOverlay(mCurrentGroup.getGroupName());
                }
                performLocationsRequest(true);
            }
        }
    }

    private class GetLocationsRequestListener extends RequestListener<GetLocationsResponse> {

        private boolean mmAutoZoom;

        public GetLocationsRequestListener(boolean autoZoom) {
            super(getActivity());
            mmAutoZoom = autoZoom;
        }

        @Override
        protected void onRequestComplete(GetLocationsResponse response) {
            mAllLocations = response.getLocations();
            getActivity().runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    populateMap(mmAutoZoom, false);
                }
            });
        }
    }
}

P.S. I realize it probably isn't the best practice to sort of hijack view creation and inject my own overlay that way, but for what it's worth, I tried commenting that portion out and it didn't solve the problem, so I'm doubtful that it's related.

like image 436
Alex Avatar asked Jan 16 '15 06:01

Alex


1 Answers

I've experienced similar problem. Let's say I have FragmentA with child SupportMapFragment. Through navigation drawer menu I invoke a dialog to be shown. At this point child map in FragmentA goes to some "background mode" - it updates approximately twice a second (I have animated markers on this map, so this behavior is well known to me). This "background" mode is turned on by showing any element with overlay, f.e. dialog or AppBar menu, and is off when that element is hidden.

In dialog i have list with items, which are starting fragment replace transaction, when clicked. At this point FragmentA is being replaced by FragmentB. Here's code snippet of OnClickListener:

final SmartFragment fragment = new SmartFragment();
FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
supportFragmentManager
        .beginTransaction()
        .replace(R.id.fragment_container, fragment, SmartFragment.FRAGMENT_TAG)
        .addToBackStack(null)
        .commit();
dismiss();

But after I return to FragmentA from backstack, map, which was retained, stays in that "background mode", even so the dialog doesn't exist anymore. Moving dissmiss() statement before transaction doesn't help, it seams like dialog isn't dismissed till all the code is executed. So the only reasonable solution (except turning of "background mode" directly on map fragment, which I can't find how to do) is to delay fragment transaction until dialog is really dismissed. As I assumed earlier, it happens only when all code in OnClick (which is a part of dialog) is executed, so we need to make transaction not being done at an instance it was declared, but a bit later, placing it in Runnable:

dismiss();
final SmartFragment fragment = new SmartFragment();
Handler handler = new Handler();
final FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
handler.post(new Runnable() {
    @Override
    public void run() {
        supportFragmentManager
                .beginTransaction()
                .replace(R.id.fragment_container, fragment, StopsSmartFragment.FRAGMENT_TAG)
                .addToBackStack(null)
                .commit();
    }
});

For this we need to make our fragment and supportFragmentManager final. Now everything works, map's "background mode" is turned off, and if you return to FragmentA from backstack, map works as it should.

If anyone has an idea how to turn off "background mode" in map fragment directly, please tell, I'll be happy to hear.

like image 84
d.u.a.l Avatar answered Sep 29 '22 23:09

d.u.a.l