読者です 読者をやめる 読者になる 読者になる

Google Map api v2 Android utility libraryをちょっと触ってみた

android googlemap gradle android-maps-utils

なんでこのLibraryが作られたの?

Markerピンの処理というと

らへんで出てくる

  • GoogleMap::addMarker が同期処理で凄く重い
  • Marker数を立てまくるとOOMや応答なしになる事が多いため

で 利点的には

  • ClusterManager::addItem 登録を受け付けるだけ
    • 順次非同期でMarker作成処理が行われる
    • 集合マーカー化することによりMarker数が必然的に減るので軽い
    • 集合マーカー条件は、マーカー数でしかできない。*1

導入的な処

dependencies {
    compile 'com.google.maps.android:android-maps-utils:0.4.+'
}

ググった場合 v3のjavascriptベースの話がおおくてもんにょりする><

実装ベース的な話

  • カスタムClusterManager(extends ClusterManager)
setRenderer(<<カスタムRenderer>>)
googleMap.setOnCameraIdleListener(this) //カメラが停止した時
googleMap.setOnMarkerClickListener(this) //マーカーをクリックした時(A)
googleMap.setOnInfoWindowClickListener(this)//InfoWindowクリック(B)
googleMap.setInfoWindowAdapter(getMarkerManager());//infoWindow自体の関連付け(C)

//集合マーカクリック時(Aの処理が分岐)
setOnClusterClickListener
//単一マーカクリック時(Aの処理が分岐)
setOnClusterItemClickListener

//集合マーカinfoWindow(Bの処理が分岐)
setOnClusterInfoWindowClickListener
//単一マーカinfoWindow(Bの処理が分岐)
setOnClusterItemInfoWindowClickListener


//集合マーカinfoWindow(Cの処理が分岐)
getClusterMarkerCollection().setOnInfoWindowAdapter
//単一マーカinfoWindow(Cの処理が分岐)
getMarkerCollection().setOnInfoWindowAdapter
  • カスタムRenderer(extends DefaultClusterRenderer)

の二本立てで実装するのがすっきりするイメージ

mClusterManager.cluster();

で再表示する

ただし、mClusterManager.addItem を1回でもしていないと

mClusterManager.cluster();の実行自体はSkipされる挙動

マーカアイコンのカスタマイズ

のお話

  • DefaultClusterRenderer::onBeforeClusterRendered
    • 集合マーカ表示直前
  • DefaultClusterRenderer::onBeforeClusterItemRendered
    • 単一マーカ表示直前

このタイミングで、markerOptionsを設定してやる

super.onBeforeClusterRendered(cluster, ,markerOptions)

を呼ばないと適応されない*2

集合マーカー表示するか否か

  • DefaultClusterRenderer::shouldRenderAsCluster
   private final static int CLUSTER_SPOT=100

    @Override
    protected boolean shouldRenderAsCluster(Cluster cluster) {
        return cluster.getSize() > 100;
    }
  • サンプルだと単一マーカの個数で集合マーカー化するみたいな記述になる
    • 上記だと100個以上の近接マーカー => 集合マーカ化する

という説明になる。

特定条件で集合マーカー化したくなければ、フラグかなんか持たせて return false を返せば多分いけるはず。

ただこれだと片手落ちで、上記の100個単位にした場合、表示表記も書き換えてやらないと今一*3

標準だと getBucket関数で使われている内部配列に最適化されている*4

書き換えるとしたら

  • DefaultClusterRenderer::getColor
  • DefaultClusterRenderer::getClusterText
  • DefaultClusterRenderer::getBucket(Cluster cluster) {

あたりか・・

   @Override
    protected int getBucket(Cluster<T> cluster) {
        int size = cluster.getSize();
        int dispSize = CLUSTER_SPOT;
        if (size <= dispSize {
            return size;
        }
        int cnt = 0;
        while(size > dispSize){
            dispSize += CLUSTER_SPOT;
            cnt++;
        }

        return cnt * CLUSTER_SPOT;//dispSizeよりひとつ減らした値を表示する
    }
    
    protected String getClusterText(int bucket) {
        if (bucket < CLUSTER_SPOT) {
            return String.valueOf(bucket);
        }
        return String.valueOf(bucket) + "+";
    }

    

最大ズームのときは、Cluster表示解除したい

集合マーカーの距離制御のプロパティはみるからないようなので、

if(map.getCameraPosition().zoom == map.getMaxZoomLevel()){
    return false;
}

みたいなコードを、shouldRenderAsCluster辺りでかけば、とりあえず動きそうなんだけど

mapに対するアクセスはUIスレッドである必要があるので、ココらへんの判定

OnCameraIdle に突っ込むしか無いのかなーというのが仕方なしな状況

画面内のみ表示する制御

Best way to render only the visible cluster items on a google map (Android) - Codedump.io

の話

ただこれだと意図通りには動かなくて

  • DefaultClusterRenderer::onClusterRendered
    • 集合マーカ表示時
  • DefaultClusterRenderer::onClusterItemRendered
    • 単一マーカ表示時

の段階でしか Markerの実体できていないはずなんですけど・・・。

下記の判定自体は使えるんですけどね。

private Boolean isInBounds(LatLng position, LatLngBounds latLngBounds) {
    return (latLngBounds == null ? mMap.getProjection().getVisibleRegion().latLngBounds : latLngBounds).contains(position);
}

で ClusterManagerのコードを読んでると

    @Override
    public void onCameraIdle() {
        if (mRenderer instanceof GoogleMap.OnCameraIdleListener) {
            ((GoogleMap.OnCameraIdleListener) mRenderer).onCameraIdle();
        }
        
        //ClusterManagerの処理
        
    }   

と GoogleMap.OnCameraIdleListener を Render側に impliments すれば割り込みが入れられると。このタイミングで判定すれば良い

onCameraIdle は UIThread 上で実行される関数のようです。*5

   @Override
    public void onCameraIdle() {
        final LatLngBounds latLngBounds = mMap.getProjection().getVisibleRegion().latLngBounds;

        // 集合マーカ only => 数が少ないので制御要らないかも??
        Collection<Marker> clusters = mClusterManager.getClusterMarkerCollection().getMarkers();
        for(Marker marker : clusters) {
            if(isInBounds(marker.getPosition(), latLngBounds)){
                marker.setVisible(true);
            }
            else{
                marker.setVisible(false);
            }
        }

        // 単一マーカー only
        Collection<Marker> markers = mClusterManager.getMarkerCollection().getMarkers();
        for(Marker marker : markers) {
            if(isInBounds(marker.getPosition(), latLngBounds)){
                marker.setVisible(true);
            }
            else{
                marker.setVisible(false);
            }
        }
    }

でもまあ正直な処、集合マーカーは大した数にはならないので、制御対象外にしてもいいかもしれない。

  • onBeforeClusterItemRendered
    • => marker.setVisible に変更したのは
    • スクロール時の表示を滑らかに見えるようにするため

こうしないと

  • MarkerOptionの設定が変更されてしまう*6
  • Markerの作り直しだと、markerIdがずれる恐れがある

でもまあ実質的に2万個とかマーカー持てば、それはそれで画面レンダリングが重くなるわけで *7

  • 毎回クリアして、画面内だけ作り直す
    • ただタイミング的にOnCameraIdleぐらいしか、処理を突っ込めないので
    • カメラが止まった時点で、ぱぱっとピン表示されるカクついた動きになってしまう

というのも手かと

*1:マーカー間距離とか指定できるとベストなんだけど。。

*2:super.onBeforeClusterItemRendered は偶々親の方に処理が無いから問題ないよう

*3:たぶん色制御も

*4:10+から1000+まで

*5:したがってgoogleMapに対するアクセス参照が可能

*6:とくに画像アイコンとか設定している場合。まあ同じ処理を書けばいいだけでは有りますが。。

*7:ただ画面表示はスムーズでカクつかない