Bu soruna şanssız bir çözüm arıyordum, bu yüzden burada sizinle paylaşmak istediğim kendimi yuvarlamak zorunda kaldım. (Lütfen kötü İngilizcemi affedin) (İngilizce bir başka Çek adamına cevap vermek biraz çılgınca :-))
Denediğim ilk şey iyi bir eski kullanmaktı PopupWindow
. Oldukça kolay - kişi sadece işaretçiyi dinlemeli OnMarkerClickListener
ve sonra bir özel göstermelidir PopupWindow
. StackOverflow'daki diğer bazı kişiler bu çözümü önerdi ve aslında ilk bakışta oldukça iyi görünüyor. Ancak bu çözümdeki sorun, haritayı hareket ettirmeye başladığınızda ortaya çıkıyor. PopupWindow
Mümkün olan bir şekilde kendinizi hareket ettirmelisiniz (bazı onTouch olaylarını dinleyerek) ama IMHO, özellikle bazı yavaş cihazlarda yeterince iyi görünmesini sağlayamazsınız. Eğer bunu basit bir şekilde bir noktadan diğerine "atlar". Bu atlamaları cilalamak için bazı animasyonlar da kullanabilirsiniz, ancak bu şekilde PopupWindow
her zaman "sevmediğim" haritada olması gereken yerde "bir adım" olacak.
Bu noktada başka bir çözüm düşünüyordum. Aslında bu kadar özgürlüğe gerçekten ihtiyacım olmadığını fark ettim - özel görünümlerimi beraberinde gelen tüm olanaklarla göstermek için (animasyonlu ilerleme çubukları gibi). Google mühendislerinin bile bunu Google Haritalar uygulamasında bu şekilde yapmamalarının iyi bir nedeni olduğunu düşünüyorum. Tek ihtiyacım olan InfoWindow'da basılı bir durumu gösterecek ve tıklandığında bazı eylemleri tetikleyecek bir düğme ya da iki. Bu yüzden iki parçaya ayrılan başka bir çözüm buldum:
İlk bölüm:
İlk bölüm, bazı eylemleri tetiklemek için düğmelere tıklamaları yakalayabilmektir. Benim fikrim şöyledir:
- InfoWindowAdapter içinde oluşturulan özel infoWindow'a bir referans tutun.
- Özel bir ViewGroup içine sarma
MapFragment
(veya MapView
) (benimki MapWrapperLayout olarak adlandırılır)
MapWrapperLayout
'S dispatchTouchEvent öğesini geçersiz kılın ve (InfoWindow şu anda gösteriliyorsa) ilk olarak MotionEvents öğesini önceden oluşturulan InfoWindow'a yönlendirin. MotionEvents'i tüketmezse (InfoWindow içindeki herhangi bir tıklanabilir alanı tıklamamış olmanız gibi) o zaman (ve ancak o zaman) olayların MapWrapperLayout'un üst sınıfına inmesini sağlayın, böylece sonunda haritaya teslim edilecektir.
MapWrapperLayout'un kaynak kodu şöyledir:
package com.circlegate.tt.cg.an.lib.map;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Marker;
import android.content.Context;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.RelativeLayout;
public class MapWrapperLayout extends RelativeLayout {
/**
* Reference to a GoogleMap object
*/
private GoogleMap map;
/**
* Vertical offset in pixels between the bottom edge of our InfoWindow
* and the marker position (by default it's bottom edge too).
* It's a good idea to use custom markers and also the InfoWindow frame,
* because we probably can't rely on the sizes of the default marker and frame.
*/
private int bottomOffsetPixels;
/**
* A currently selected marker
*/
private Marker marker;
/**
* Our custom view which is returned from either the InfoWindowAdapter.getInfoContents
* or InfoWindowAdapter.getInfoWindow
*/
private View infoWindow;
public MapWrapperLayout(Context context) {
super(context);
}
public MapWrapperLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MapWrapperLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* Must be called before we can route the touch events
*/
public void init(GoogleMap map, int bottomOffsetPixels) {
this.map = map;
this.bottomOffsetPixels = bottomOffsetPixels;
}
/**
* Best to be called from either the InfoWindowAdapter.getInfoContents
* or InfoWindowAdapter.getInfoWindow.
*/
public void setMarkerWithInfoWindow(Marker marker, View infoWindow) {
this.marker = marker;
this.infoWindow = infoWindow;
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean ret = false;
// Make sure that the infoWindow is shown and we have all the needed references
if (marker != null && marker.isInfoWindowShown() && map != null && infoWindow != null) {
// Get a marker position on the screen
Point point = map.getProjection().toScreenLocation(marker.getPosition());
// Make a copy of the MotionEvent and adjust it's location
// so it is relative to the infoWindow left top corner
MotionEvent copyEv = MotionEvent.obtain(ev);
copyEv.offsetLocation(
-point.x + (infoWindow.getWidth() / 2),
-point.y + infoWindow.getHeight() + bottomOffsetPixels);
// Dispatch the adjusted MotionEvent to the infoWindow
ret = infoWindow.dispatchTouchEvent(copyEv);
}
// If the infoWindow consumed the touch event, then just return true.
// Otherwise pass this event to the super class and return it's result
return ret || super.dispatchTouchEvent(ev);
}
}
Tüm bunlar InfoView içindeki görünümleri tekrar "canlı" hale getirecektir - OnClickListeners tetiklemeye başlayacaktır.
İkinci kısım:
Kalan sorun, açıkça, InfoWindow'unuzun UI değişikliklerini ekranda göremediğinizdir. Bunu yapmak için Marker.showInfoWindow'u manuel olarak aramanız gerekir. Şimdi, InfoWindow'unuzda kalıcı bir değişiklik yaparsanız (düğmenizin etiketini başka bir şeye değiştirmek gibi), bu yeterince iyidir.
Ancak bir düğmeye basılmış durum veya bu tür bir şey göstermek daha karmaşıktır. İlk sorun, (en azından) InfoWindow'un normal düğmenin basılı durumunu göstermesini sağlayamadım. Düğmeye uzun bir süre basmış olsam bile, ekranda henüz basılı kalmadı. Bunun, muhtemelen bilgi pencerelerinde herhangi bir geçici durum göstermemeyi sağlayan harita çerçevesinin kendisi tarafından işlenen bir şey olduğuna inanıyorum. Ama yanılmış olabilirim, bunu bulmaya çalışmadım.
Yaptığım başka bir kötü kesmek - ekli bir OnTouchListener
Düğmeye bir ekledim ve düğmeye basıldığında veya iki özel çekmeceye bırakıldığında manuel olarak arka planını değiştirdim - biri normal durumda ve diğeri basılı durumda. Bu çok hoş değil, ama işe yarıyor :). Artık düğmenin ekranda normalden basılı durumlara geçişini görebiliyordum.
Hala son bir aksaklık var - düğmeyi çok hızlı tıklatırsanız, basılı durumu göstermez - sadece normal durumunda kalır (tıklamanın kendisi tetiklenir, ancak düğme "çalışır"). En azından Galaxy Nexus'umda böyle görünüyor. Yaptığım son şey, düğmeye basıldığında düğmeyi biraz geciktirdim. Bu da oldukça çirkin ve bazı eski, yavaş cihazlarda nasıl çalışacağından emin değilim ama harita çerçevesinin bile böyle bir şey yaptığını sanıyorum. Bunu kendiniz deneyebilirsiniz - InfoWindow'un tamamını tıkladığınızda, basılı bir durumda biraz daha uzun kalır, sonra normal düğmeler yapar (tekrar - en azından telefonumda). Aslında bu, orijinal Google Haritalar uygulamasında bile böyle çalışır.
Neyse, düğmelerdeki durum değişikliklerini ve bahsettiğim diğer şeyleri işleyen özel bir sınıf yazdım, işte kod:
package com.circlegate.tt.cg.an.lib.map;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import com.google.android.gms.maps.model.Marker;
public abstract class OnInfoWindowElemTouchListener implements OnTouchListener {
private final View view;
private final Drawable bgDrawableNormal;
private final Drawable bgDrawablePressed;
private final Handler handler = new Handler();
private Marker marker;
private boolean pressed = false;
public OnInfoWindowElemTouchListener(View view, Drawable bgDrawableNormal, Drawable bgDrawablePressed) {
this.view = view;
this.bgDrawableNormal = bgDrawableNormal;
this.bgDrawablePressed = bgDrawablePressed;
}
public void setMarker(Marker marker) {
this.marker = marker;
}
@Override
public boolean onTouch(View vv, MotionEvent event) {
if (0 <= event.getX() && event.getX() <= view.getWidth() &&
0 <= event.getY() && event.getY() <= view.getHeight())
{
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN: startPress(); break;
// We need to delay releasing of the view a little so it shows the pressed state on the screen
case MotionEvent.ACTION_UP: handler.postDelayed(confirmClickRunnable, 150); break;
case MotionEvent.ACTION_CANCEL: endPress(); break;
default: break;
}
}
else {
// If the touch goes outside of the view's area
// (like when moving finger out of the pressed button)
// just release the press
endPress();
}
return false;
}
private void startPress() {
if (!pressed) {
pressed = true;
handler.removeCallbacks(confirmClickRunnable);
view.setBackground(bgDrawablePressed);
if (marker != null)
marker.showInfoWindow();
}
}
private boolean endPress() {
if (pressed) {
this.pressed = false;
handler.removeCallbacks(confirmClickRunnable);
view.setBackground(bgDrawableNormal);
if (marker != null)
marker.showInfoWindow();
return true;
}
else
return false;
}
private final Runnable confirmClickRunnable = new Runnable() {
public void run() {
if (endPress()) {
onClickConfirmed(view, marker);
}
}
};
/**
* This is called after a successful click
*/
protected abstract void onClickConfirmed(View v, Marker marker);
}
İşte kullandığım özel bir InfoWindow düzeni dosyası:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginRight="10dp" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="18sp"
android:text="Title" />
<TextView
android:id="@+id/snippet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="snippet" />
</LinearLayout>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Button" />
</LinearLayout>
Test etkinlik düzeni dosyası ( MapFragment
içinde olmak MapWrapperLayout
):
<com.circlegate.tt.cg.an.lib.map.MapWrapperLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/map_relative_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<fragment
android:id="@+id/map"
android:layout_width="match_parent"
android:layout_height="match_parent"
class="com.google.android.gms.maps.MapFragment" />
</com.circlegate.tt.cg.an.lib.map.MapWrapperLayout>
Ve son olarak, tüm bunları bir araya getiren bir test etkinliğinin kaynak kodu:
package com.circlegate.testapp;
import com.circlegate.tt.cg.an.lib.map.MapWrapperLayout;
import com.circlegate.tt.cg.an.lib.map.OnInfoWindowElemTouchListener;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter;
import com.google.android.gms.maps.MapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends Activity {
private ViewGroup infoWindow;
private TextView infoTitle;
private TextView infoSnippet;
private Button infoButton;
private OnInfoWindowElemTouchListener infoButtonListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final MapFragment mapFragment = (MapFragment)getFragmentManager().findFragmentById(R.id.map);
final MapWrapperLayout mapWrapperLayout = (MapWrapperLayout)findViewById(R.id.map_relative_layout);
final GoogleMap map = mapFragment.getMap();
// MapWrapperLayout initialization
// 39 - default marker height
// 20 - offset between the default InfoWindow bottom edge and it's content bottom edge
mapWrapperLayout.init(map, getPixelsFromDp(this, 39 + 20));
// We want to reuse the info window for all the markers,
// so let's create only one class member instance
this.infoWindow = (ViewGroup)getLayoutInflater().inflate(R.layout.info_window, null);
this.infoTitle = (TextView)infoWindow.findViewById(R.id.title);
this.infoSnippet = (TextView)infoWindow.findViewById(R.id.snippet);
this.infoButton = (Button)infoWindow.findViewById(R.id.button);
// Setting custom OnTouchListener which deals with the pressed state
// so it shows up
this.infoButtonListener = new OnInfoWindowElemTouchListener(infoButton,
getResources().getDrawable(R.drawable.btn_default_normal_holo_light),
getResources().getDrawable(R.drawable.btn_default_pressed_holo_light))
{
@Override
protected void onClickConfirmed(View v, Marker marker) {
// Here we can perform some action triggered after clicking the button
Toast.makeText(MainActivity.this, marker.getTitle() + "'s button clicked!", Toast.LENGTH_SHORT).show();
}
};
this.infoButton.setOnTouchListener(infoButtonListener);
map.setInfoWindowAdapter(new InfoWindowAdapter() {
@Override
public View getInfoWindow(Marker marker) {
return null;
}
@Override
public View getInfoContents(Marker marker) {
// Setting up the infoWindow with current's marker info
infoTitle.setText(marker.getTitle());
infoSnippet.setText(marker.getSnippet());
infoButtonListener.setMarker(marker);
// We must call this to set the current marker and infoWindow references
// to the MapWrapperLayout
mapWrapperLayout.setMarkerWithInfoWindow(marker, infoWindow);
return infoWindow;
}
});
// Let's add a couple of markers
map.addMarker(new MarkerOptions()
.title("Prague")
.snippet("Czech Republic")
.position(new LatLng(50.08, 14.43)));
map.addMarker(new MarkerOptions()
.title("Paris")
.snippet("France")
.position(new LatLng(48.86,2.33)));
map.addMarker(new MarkerOptions()
.title("London")
.snippet("United Kingdom")
.position(new LatLng(51.51,-0.1)));
}
public static int getPixelsFromDp(Context context, float dp) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int)(dp * scale + 0.5f);
}
}
Bu kadar. Şimdiye kadar bunu sadece Galaxy Nexus (4.2.1) ve Nexus 7 (ayrıca 4.2.1) üzerinde test ettim, bir şansım olduğunda bazı Gingerbread telefonlarında deneyeceğim. Şimdiye kadar bulduğum bir sınırlama, haritayı ekranınızdaki düğmenin olduğu yerden sürükleyememeniz ve haritayı hareket ettirememenizdir. Muhtemelen bir şekilde aşılabilir ama şimdilik bununla yaşayabilirim.
Bunun çirkin bir hack olduğunu biliyorum ama daha iyi bir şey bulamadım ve bu tasarım desenine o kadar kötü ihtiyacım var ki, bu gerçekten harita v1 çerçevesine geri dönmek için bir neden olacaktır (ki btw. Gerçekten kaçınmak istiyorum) parçaları vb. ile yeni bir uygulama için). Google'ın neden geliştiricilere InfoWindows'da bir düğmeye sahip olmanın resmi bir yolunu sunmadığını anlamıyorum. Bu yaygın bir tasarım deseni, üstelik bu desen resmi Google Haritalar uygulamasında bile kullanılıyor :). InfoWindows'da yalnızca görüşlerinizi "canlı" hale getirememelerinin nedenlerini anlıyorum - bu muhtemelen haritayı hareket ettirirken ve kaydırırken performansı öldürür. Ancak, bu etkiyi görünüm kullanmadan elde etmenin bir yolu olmalı.