मैं खुद इस समस्या का हल ढूंढ रहा था जिसमें कोई भाग्य नहीं था, इसलिए मुझे अपना रोल करना पड़ा जिसे मैं आपके साथ साझा करना चाहूंगा। (कृपया मेरी खराब अंग्रेजी को माफ करें) (यह अंग्रेजी में एक और चेक लड़के का जवाब देने के लिए थोड़ा पागल है :-))
पहली चीज जो मैंने कोशिश की वह थी एक अच्छे पुराने का उपयोग करना PopupWindow
। यह काफी आसान है - केवल एक को सुनना पड़ता है OnMarkerClickListener
और फिर PopupWindow
मार्कर के ऊपर एक कस्टम दिखाना होता है । StackOverflow पर कुछ अन्य लोगों ने इस समाधान का सुझाव दिया और यह वास्तव में पहली नज़र में काफी अच्छा लग रहा है। लेकिन इस समाधान के साथ समस्या तब दिखाई देती है जब आप नक्शे को चारों ओर ले जाना शुरू करते हैं। आपको PopupWindow
किसी तरह अपने आप को स्थानांतरित करना होगा जो संभव है (कुछ ऑनटच घटनाओं को सुनकर) लेकिन IMHO आप इसे पर्याप्त रूप से अच्छा नहीं बना सकते हैं, खासकर कुछ धीमे उपकरणों पर। यदि आप इसे सरल तरीके से करते हैं तो यह एक स्थान से दूसरे स्थान पर "कूदता है"। आप उन छलांगों को चमकाने के लिए कुछ एनिमेशन का भी उपयोग कर सकते हैं लेकिन इस तरह से PopupWindow
यह हमेशा "पीछे एक कदम" होगा जहां यह उस नक्शे पर होना चाहिए जो मुझे पसंद नहीं है।
इस बिंदु पर, मैं कुछ अन्य समाधान के बारे में सोच रहा था। मुझे एहसास हुआ कि मुझे वास्तव में इतनी आजादी की आवश्यकता नहीं है - अपने कस्टम विचारों को इसके साथ आने वाली सभी संभावनाओं के साथ दिखाने के लिए (जैसे एनिमेटेड प्रगति बार आदि)। मुझे लगता है कि एक अच्छा कारण है कि Google इंजीनियर भी Google मैप्स ऐप में ऐसा नहीं करते हैं। सभी की जरूरत है मैं एक बटन या दो InfoWindow पर एक दबाया हुआ राज्य दिखाऊंगा और क्लिक किए जाने पर कुछ क्रियाओं को ट्रिगर करूंगा। इसलिए मैं एक और समाधान के साथ आया जो दो भागों में विभाजित है:
पहला भाग:
पहला भाग कुछ क्रियाओं को गति देने के लिए बटनों पर क्लिकों को पकड़ने में सक्षम होता है। मेरा विचार इस प्रकार है:
- InfoWindowAdapter में बनाए गए कस्टम infoWindow का संदर्भ रखें।
- कस्टम व्यूग्रुप के अंदर
MapFragment
(या MapView
) लपेटें (मेरा नाम MapWrapperLayout है)
- ओवरराइड
MapWrapperLayout
पूर्व में बने InfoWindow करने के dispatchTouchEvent और (InfoWindow वर्तमान में प्रदर्शित किया गया है) पहले मार्ग MotionEvents। यदि यह MotionEvents का उपभोग नहीं करता है (जैसे कि क्योंकि आप InfoWindow आदि के अंदर किसी भी क्लिक करने योग्य क्षेत्र पर क्लिक नहीं करते हैं) तब (और केवल तब) इवेंट्स को MapWrapperLayout के सुपरक्लास पर जाने दें ताकि यह अंततः मैप पर पहुंच जाए।
यहाँ MapWrapperLayout का स्रोत कोड है:
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);
}
}
यह सब InfoView "लाइव" के अंदर फिर से विचार कर देगा - OnClickListeners शुरू हो जाएगा आदि।
दूसरा भाग:
शेष समस्या यह है कि जाहिर है, आप स्क्रीन पर अपने InfoWindow का कोई UI परिवर्तन नहीं देख सकते हैं। ऐसा करने के लिए आपको मैन्युअल रूप से Marker.showInfoWindow पर कॉल करना होगा। अब, यदि आप अपने InfoWindow में कुछ स्थायी परिवर्तन करते हैं (जैसे कि आपके बटन के लेबल को किसी और चीज़ में बदलना), तो यह काफी अच्छा है।
लेकिन एक बटन दबाया गया राज्य या उस प्रकृति का कुछ दिखाना अधिक जटिल है। पहली समस्या यह है, कि (कम से कम) मैं InfoWindow को सामान्य बटन के दबाए जाने की स्थिति दिखाने में सक्षम नहीं था। यहां तक कि अगर मैंने बटन को लंबे समय तक दबाया, तो भी यह स्क्रीन पर अप्रभावित रहा। मेरा मानना है कि यह कुछ ऐसा है जो मैप फ्रेमवर्क द्वारा ही संभाला जाता है, जो संभवत: यह सुनिश्चित करता है कि सूचना विंडो में कोई क्षणिक स्थिति न दिखाए। लेकिन मैं गलत हो सकता है, मैंने यह पता लगाने की कोशिश नहीं की।
मैंने जो किया वह एक और गंदा हैक है - मैंने OnTouchListener
बटन को संलग्न किया और मैन्युअल रूप से इसे तब स्विच किया जब बटन दबाया गया था या दो कस्टम ड्रॉबल्स के लिए जारी किया गया था - एक सामान्य अवस्था में एक बटन के साथ और दूसरा एक दबाए गए राज्य में। यह बहुत अच्छा नहीं है, लेकिन यह काम करता है :)। अब मैं स्क्रीन पर सामान्य से दबाए गए राज्यों के बीच स्विच करने वाले बटन को देख पा रहा था।
अभी भी एक अंतिम गड़बड़ है - यदि आप बटन को बहुत तेज़ी से क्लिक करते हैं, तो यह दबाया गया राज्य नहीं दिखाता है - यह सिर्फ अपनी सामान्य स्थिति में रहता है (हालाँकि क्लिक ही निकाल दिया जाता है इसलिए बटन "काम करता है")। कम से कम यह मेरी गैलेक्सी नेक्सस पर दिखाता है। इसलिए मैंने जो आखिरी काम किया, वह यह है कि मैंने बटन दबाया, उसमें थोड़ी-बहुत दबाया गया। यह भी काफी बदसूरत है और मुझे यकीन नहीं है कि यह कुछ पुराने, धीमे उपकरणों पर कैसे काम करेगा लेकिन मुझे संदेह है कि यहां तक कि मैप का ढांचा भी इसके लिए कुछ करता है। आप इसे स्वयं आज़मा सकते हैं - जब आप पूरी जानकारी क्लिक करते हैं, तो यह थोड़ी देर तक दबाए रहता है, फिर सामान्य बटन करते हैं (फिर से - कम से कम मेरे फोन पर)। और यह वास्तव में मूल Google मैप्स ऐप पर भी काम करता है।
वैसे भी, मैंने खुद को एक कस्टम वर्ग लिखा है जो बटन स्थिति परिवर्तन और मेरे द्वारा उल्लिखित अन्य सभी चीजों को संभालता है, इसलिए यहां कोड है:
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);
}
यहाँ एक कस्टम InfoWindow लेआउट फ़ाइल है जिसका मैंने उपयोग किया है:
<?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>
टेस्ट गतिविधि लेआउट फ़ाइल ( MapFragment
अंदर होना 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>
और अंत में एक परीक्षण गतिविधि का स्रोत कोड, जो इस सब को एक साथ जोड़ देता है:
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);
}
}
बस। अब तक मैंने केवल अपने गैलेक्सी नेक्सस (4.2.1) और नेक्सस 7 (4.2.1) पर भी इसका परीक्षण किया है, मैं मौका मिलने पर कुछ जिंजरब्रेड फोन पर इसकी कोशिश करूंगा। एक सीमा मुझे अब तक मिली है कि आप मानचित्र को स्क्रीन पर अपने बटन को नहीं खींच सकते हैं और मानचित्र को चारों ओर ले जा सकते हैं। यह शायद किसी तरह से दूर किया जा सकता है लेकिन अभी के लिए, मैं उस के साथ रह सकता हूं।
मुझे पता है कि यह एक बदसूरत हैक है, लेकिन मुझे अभी कुछ बेहतर नहीं मिला है और मुझे इस डिज़ाइन पैटर्न की इतनी बुरी तरह से आवश्यकता है कि यह वास्तव में मैप v1 फ्रेमवर्क में वापस जाने का एक कारण होगा (जो कि btw। मैं वास्तव में बचना चाहूंगा। टुकड़े आदि के साथ एक नए अनुप्रयोग के लिए)। मुझे अभी समझ में नहीं आया कि Google डेवलपर्स को InfoWindows पर एक बटन रखने के लिए कुछ आधिकारिक तरीके की पेशकश क्यों नहीं करता है। यह एक ऐसा सामान्य डिज़ाइन पैटर्न है, इसके अलावा इस पैटर्न का उपयोग आधिकारिक Google मैप्स ऐप :) में भी किया जाता है। मैं उन कारणों को समझता हूं कि वे आपके विचारों को InfoWindows में "लाइव" क्यों नहीं कर सकते हैं - यह संभवतया प्रदर्शन को मार देगा जब नक्शे को चारों ओर ले जाना और स्क्रॉल करना होगा। लेकिन विचारों का उपयोग किए बिना इस प्रभाव को प्राप्त करने का कोई तरीका होना चाहिए।