重复ID、标签为空或父ID与另一个片段相同,适用于com.google.android.gms.maps.MapFragment。

344
我有一个包含三个选项卡的应用程序。
每个选项卡都有自己的布局.xml文件。main.xml拥有自己的地图碎片,这是应用程序首次启动时显示的选项卡。
除了切换选项卡时出现问题外,一切正常。如果我尝试切换回地图碎片选项卡,就会出现错误。在其他选项卡之间切换则没有问题。
可能出了什么问题?
这是我的主类、main.xml以及我使用的相关类(您还将在底部找到错误日志)。
package com.nfc.demo;

import android.app.ActionBar;
import android.app.ActionBar.Tab;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.os.Bundle;
import android.widget.Toast;

public class NFCDemoActivity extends Activity {

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ActionBar bar = getActionBar();
        bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
        bar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);

        bar.addTab(bar
                .newTab()
                .setText("Map")
                .setTabListener(
                        new TabListener<MapFragment>(this, "map",
                                MapFragment.class)));
        bar.addTab(bar
                .newTab()
                .setText("Settings")
                .setTabListener(
                        new TabListener<SettingsFragment>(this, "settings",
                                SettingsFragment.class)));
        bar.addTab(bar
                .newTab()
                .setText("About")
                .setTabListener(
                        new TabListener<AboutFragment>(this, "about",
                                AboutFragment.class)));

        if (savedInstanceState != null) {
            bar.setSelectedNavigationItem(savedInstanceState.getInt("tab", 0));
        }
        // setContentView(R.layout.main);

    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("tab", getActionBar().getSelectedNavigationIndex());
    }

    public static class TabListener<T extends Fragment> implements
            ActionBar.TabListener {
        private final Activity mActivity;
        private final String mTag;
        private final Class<T> mClass;
        private final Bundle mArgs;
        private Fragment mFragment;

        public TabListener(Activity activity, String tag, Class<T> clz) {
            this(activity, tag, clz, null);
        }

        public TabListener(Activity activity, String tag, Class<T> clz,
                Bundle args) {
            mActivity = activity;
            mTag = tag;
            mClass = clz;
            mArgs = args;

            // Check to see if we already have a fragment for this tab,
            // probably from a previously saved state. If so, deactivate
            // it, because our initial state is that a tab isn't shown.
            mFragment = mActivity.getFragmentManager().findFragmentByTag(mTag);
            if (mFragment != null && !mFragment.isDetached()) {
                FragmentTransaction ft = mActivity.getFragmentManager()
                        .beginTransaction();
                ft.detach(mFragment);
                ft.commit();
            }
        }

        public void onTabSelected(Tab tab, FragmentTransaction ft) {
            if (mFragment == null) {
                mFragment = Fragment.instantiate(mActivity, mClass.getName(),
                        mArgs);
                ft.add(android.R.id.content, mFragment, mTag);
            } else {
                ft.attach(mFragment);
            }
        }

        public void onTabUnselected(Tab tab, FragmentTransaction ft) {
            if (mFragment != null) {
                ft.detach(mFragment);
            }
        }

        public void onTabReselected(Tab tab, FragmentTransaction ft) {
            Toast.makeText(mActivity, "Reselected!", Toast.LENGTH_SHORT)
                         .show();
        }
    }

}

main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <fragment
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/mapFragment"
        android:name="com.google.android.gms.maps.MapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</LinearLayout>

相关类(MapFragment.java)
package com.nfc.demo;

import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class MapFragment extends Fragment {

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        super.onCreateView(inflater, container, savedInstanceState);
        return inflater.inflate(R.layout.main, container, false);
    }

    public void onDestroy() {
        super.onDestroy();
    }
}

错误

android.view.InflateException: Binary XML file line #7: 
     Error inflating class fragment
   at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:704)
   at android.view.LayoutInflater.rInflate(LayoutInflater.java:746)
   at android.view.LayoutInflater.inflate(LayoutInflater.java:489)
   at android.view.LayoutInflater.inflate(LayoutInflater.java:396)
   at com.nfc.demo.MapFragment.onCreateView(MapFragment.java:15)
   at android.app.Fragment.performCreateView(Fragment.java:1695)
   at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:885)
   at android.app.FragmentManagerImpl.attachFragment(FragmentManager.java:1255)
   at android.app.BackStackRecord.run(BackStackRecord.java:672)
   at android.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1435)
   at android.app.FragmentManagerImpl$1.run(FragmentManager.java:441)
   at android.os.Handler.handleCallback(Handler.java:725)
   at android.os.Handler.dispatchMessage(Handler.java:92)
   at android.os.Looper.loop(Looper.java:137)
   at android.app.ActivityThread.main(ActivityThread.java:5039)
   at java.lang.reflect.Method.invokeNative(Native Method)
   at java.lang.reflect.Method.invoke(Method.java:511)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
   at dalvik.system.NativeStart.main(Native Method)

Caused by: java.lang.IllegalArgumentException: 
     Binary XML file line #7: Duplicate id 0x7f040005, tag null, or 
     parent id 0xffffffff with another fragment for 
     com.google.android.gms.maps.MapFragment
   at android.app.Activity.onCreateView(Activity.java:4722)
   at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:680)
   ... 19 more

尝试使用以下代码:return super.onCreateView(inflater, container, savedInstanceState); 而不是 return inflater.inflate(R.layout.main, container, false); - Nik
当savedInstanceState不为空时,您不应添加您的片段。 - muyiou
看一下这条评论,它可能会有所帮助:https://dev59.com/g-o6XIcBkEYKwwoYNBpn#31723342 - Oleksandr
2
请更改已接受的答案!您选择了一个非常糟糕的答案,会导致内存泄漏!正确的答案是:https://dev59.com/Qeo6XIcBkEYKwwoYOR0q#19815266 - Daniele Segato
23个回答

400

Matt建议的答案是可行的,但它可能会导致地图被重新创建和重绘,这并不总是理想的。 经过多次尝试,我找到了适合我的解决方案:

private static View view;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    if (view != null) {
        ViewGroup parent = (ViewGroup) view.getParent();
        if (parent != null)
            parent.removeView(view);
    }
    try {
        view = inflater.inflate(R.layout.map, container, false);
    } catch (InflateException e) {
        /* map is already there, just return view as it is */
    }
    return view;
}

为了确保准确性,这里是包含 R.id.mapFragment (android:id="@+id/mapFragment") 的 "map.xml" (R.layout.map):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <fragment xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/mapFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        class="com.google.android.gms.maps.SupportMapFragment" />
</LinearLayout>

我希望这可以帮到你,但是我不能保证它不会产生任何不良影响。

编辑:存在一些负面影响,例如退出应用程序并再次启动时。由于该应用程序不一定完全关闭(而只是在后台休眠),因此以前提交的代码会在重新启动应用程序时失败。我更新了代码,现在可以正常运行了,包括进入和退出地图以及退出和重新启动应用程序,虽然我对try-catch那部分还不是很满意,但它似乎已经足够好用了。当查看堆栈跟踪时,我想到可以检查地图片段是否在FragmentManager中,不需要try-catch块,代码已更新。

更多编辑:结果发现你确实需要那个try-catch。仅仅检查地图片段并没有像预期那样好用。唉。


30
这是一个错误的答案 -1!您正在使用静态修饰符为View泄漏活动。此问题的根本原因可能是另一个泄漏的活动,由于您保留了指向它的强引用,因此无法进行垃圾回收。如果抛出InflateException,则使用上下文已销毁的活动的视图!最好在应用程序中查找其他内存泄漏,这将解决所有问题。 - tomrozb
1
我猜我们可以使用WeakReference来避免内存泄漏? - Desmond Lua
2
这对我也起作用 +1。您不必使用视图的静态引用。 - Karol Żygłowicz
5
不要这样做!这会导致内存泄漏。唯一发生这种情况的原因是你在一个片段中从XML文件中膨胀另一个片段。你不应该这样做!你应该使用ChildFragmentManager并在onViewCreated()中添加片段。 - Daniele Segato
1
是的,这确实会泄漏活动。在https://dev59.com/R-o6XIcBkEYKwwoYGwcw#27592123中找到了一个可行的解决方案。思路是在`onDestroyView`方法中手动删除`SupportMapFragment`。 - ULazdins
显示剩余2条评论

286

问题在于你正在尝试做的事情不应该被做。你不应该在其他片段中膨胀片段。根据Android的文档

注意:当布局中包含<fragment>时,您不能将布局膨胀到片段中。仅在动态添加到片段时支持嵌套片段。

虽然您可能能够通过此处提供的技巧来完成任务,但我强烈建议您不要这样做。无法确定这些技巧是否会处理每个新的Android OS,当您尝试为包含另一个片段的片段膨胀布局时。

将片段添加到另一个片段中的唯一受支持的Android方式是通过子片段管理器的事务。

只需将XML布局更改为一个空容器(如果需要,添加ID):

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/mapFragmentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
</LinearLayout>

然后在Fragment的 onViewCreated(View view, @Nullable Bundle savedInstanceState) 方法中:

@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    super.onViewCreated(view, savedInstanceState);
    FragmentManager fm = getChildFragmentManager();
    SupportMapFragment mapFragment = (SupportMapFragment) fm.findFragmentByTag("mapFragment");
    if (mapFragment == null) {
        mapFragment = new SupportMapFragment();
        FragmentTransaction ft = fm.beginTransaction();
        ft.add(R.id.mapFragmentContainer, mapFragment, "mapFragment");
        ft.commit();
        fm.executePendingTransactions();
    }
    mapFragment.getMapAsync(callback);
}

4
请参考 https://dev59.com/kWYr5IYBdhLWcg3wXJEK 来创建一个地图片段并以编程方式初始化地图的例子。 - Kristopher Johnson
1
请参阅http://stackoverflow.com/questions/19239175/java-lang-illegalstateexception-activity-has-been-destroyed-using-fragments,了解子片段支持中明显错误的解决方法。 - Kristopher Johnson
如果您刚刚从布局文件中删除了mapFragment,那么在代码中您如何仍然引用它?它被移动到其他地方了吗? - Chucky
啊,我明白了,你是通过标签而不是ID来完成的,我猜这一定是在程序中动态创建的而不是从布局文件中获取的吧? - Chucky
2
根据文档,使用SupportMapFragment.newInstance(); https://developers.google.com/maps/documentation/android-api/map - Jemshit Iskenderov
显示剩余5条评论

186

我遇到了同样的问题,通过在Fragment类的onDestroy()方法中手动删除MapFragment来解决了这个问题。以下是一段可行的代码,它通过XML中的ID引用了MapFragment:

@Override
public void onDestroyView() {
    super.onDestroyView();
    MapFragment f = (MapFragment) getFragmentManager()
                                         .findFragmentById(R.id.map);
    if (f != null) 
        getFragmentManager().beginTransaction().remove(f).commit();
}
如果不手动删除MapFragment,它会一直存在,以便不会花费大量资源重新创建/显示地图视图。保留底层的MapView似乎非常适用于在选项卡之间切换,但在片段中使用时,此行为会导致在每个具有相同ID的新MapFragment上创建重复的MapView。解决方案是手动删除MapFragment,从而在膨胀片段时每次重新创建底层地图。

我还在另一个答案中注意到了这一点[1]。


26
这个可以工作,但如果我旋转屏幕,我的应用程序会崩溃,并显示以下异常:Caused by: java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState - jramoyo
1
对于那些可能感兴趣的人,您可以通过在片段的构造函数中存储初始方向并仅在方向未更改时调用上述事务来消除屏幕旋转引起的异常。如果它已更改,则无需处理重复ID,因为在旋转后重新创建视图时不存在该ID。如果Matt不介意,我可以编辑答案并提供代码片段。 - Stan
1
嗯,对我来说地图片段没有被移除。我一定做错了什么,但如果我调用getActivity().getSupportFragmentManager().getFragments(),在调用remove(f).commit之后片段仍然存在。有人知道为什么吗?(我用getSupportFragManager替换了getFragManager) - Daniel Wilson
10
commitAllowingStateLoss 会避免 IllegalStateException 异常。 - Héctor Júdez Sapena
这解决了问题,但导致我的应用程序在退出时崩溃,同时片段仍可见。 - Sam
显示剩余7条评论

23

这是我的回答:

1、创建一个像下面这样的布局xml:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/map_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

2,在Fragment类中,以编程方式添加Google地图。

import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.SupportMapFragment;
import android.app.Activity;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

/**
 * A simple {@link android.support.v4.app.Fragment} subclass. Activities that
 * contain this fragment must implement the
 * {@link MapFragment.OnFragmentInteractionListener} interface to handle
 * interaction events. Use the {@link MapFragment#newInstance} factory method to
 * create an instance of this fragment.
 * 
 */
public class MapFragment extends Fragment {
    // TODO: Rename parameter arguments, choose names that match
    private GoogleMap mMap;

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

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_map, container, false);
        SupportMapFragment mMapFragment = SupportMapFragment.newInstance();
        mMap = mMapFragment.getMap();
        FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
        transaction.add(R.id.map_container, mMapFragment).commit();
        return view;
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        Log.d("Attach", "on attach");
    }

    @Override
    public void onDetach() {
        super.onDetach();
    }
} 

2
mMapFragment.getMap(); 返回 null。有任何想法是为什么? - Anas Azeem
@AnasAzeem,通过编程方式创建片段可以完美解决问题,也许在你的情况下不需要获取mMap来解决问题。 - ysldl
@AnasAzeem 你应该使用 getMapAsync 方法,在初始化后(在后台)正确返回地图实例。这是与谷歌地图一起工作的“正确”方式,与片段无关。 - Ganesh Krishnan

10
  1. 正如@Justin Breitfeller所提到的,@Vidar Wahlberg的解决方案可能是一种黑客方法,未来版本的Android可能无法使用。
  2. @Vidar Wahlberg倾向于使用hack方法,因为其他解决方案可能会“导致地图重新创建和重绘,这并不总是理想的”。可以通过保留旧地图片段而不是每次创建新实例来防止地图重绘。
  3. @Matt的解决方案对我不起作用(IllegalStateException)
  4. 正如@Justin Breitfeller所引用的,“当布局包含一个<fragment>时,您不能将布局填充到一个片段中。仅在动态添加到片段时支持嵌套片段。”

我的解决方案:

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,                              Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_map_list, container, false);

    // init
    //mapFragment = (SupportMapFragment)getChildFragmentManager().findFragmentById(R.id.map);
    // don't recreate fragment everytime ensure last map location/state are maintain
    if (mapFragment == null) {
        mapFragment = SupportMapFragment.newInstance();
        mapFragment.getMapAsync(this);
    }
    FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
    // R.id.map is a layout
    transaction.replace(R.id.map, mapFragment).commit();

    return view;
}

2
感谢@Desmond,你的解决方案对我非常有效。唯一需要记住的是不要在布局中创建地图。在这个解决方案中,地图的创建是嵌入在代码中的,所以将<fragment android:name="com.google.android.gms.maps.SupportMapFragment">更改为例如<LinearLayout id="@+id/map />。 - kosiara - Bartosz Kosarzycki

9

全局声明SupportMapFragment对象

    private SupportMapFragment mapFragment;

在onCreateView()方法中放置以下代码

mapFragment = (SupportMapFragment) getChildFragmentManager()
            .findFragmentById(R.id.map);
 mapFragment.getMapAsync(this);

在 onDestroyView() 中放置以下代码。
@Override
public void onDestroyView() {
   super.onDestroyView();

    if (mapFragment != null)
        getFragmentManager().beginTransaction().remove(mapFragment).commit();
}

在您的 XML 文件中添加以下代码:
 <fragment
    android:id="@+id/map"
    android:name="com.abc.Driver.fragment.FragmentHome"
    class="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

上述代码解决了我的问题,并且它正常工作。

3

另一种解决方案:

if (view == null) {
    view = inflater.inflate(R.layout.nearbyplaces, container, false);
}

就是这样,如果不为空,您无需重新初始化它,从父级中删除也是不必要的步骤。


这是我情况下最好的解决方案。由于在我的导航图中使用了两个片段,所以我一直遇到那个错误,但这个方法解决了我的问题。 - Kennedy Kambo
这是我情况下最好的解决方案。由于在我的导航图中使用了两个片段,所以我一直遇到那个错误,但这个方法解决了我的问题。 - Kennedy Kambo

3
您正在重复返回或填充布局,请检查是否只填充一次。

太棒了!我本来会找上几个小时的,但没注意到这个。谢谢 @MahdiGiveie - Noman khanbhai

3
我建议在处理选项卡时使用replace()而不是attach()/detach()
或者,您可以转换为使用ViewPager。这里有一个示例项目,展示了一个带有选项卡的ViewPager,可承载10张地图。请参见此处

但如果出现内存不足错误,使用分离窗口会导致映射问题。 - Vishal Android developer

2

我有一点晚,但是这些答案都没有对我的情况有所帮助。我在我的片段中同时使用了Google地图作为SupportMapFragmentPlaceAutocompleteFragment。所有的答案都指出问题在于SupportMapFragment需要重新创建和重绘地图。但是经过深入挖掘,我发现我的问题实际上与PlaceAutocompleteFragment有关。

因此,对于那些由于SupportMapFragmentSupportMapFragment而遇到此问题的人们,这里提供一个解决方案。

 //Global SupportMapFragment mapFragment;
 mapFragment = (SupportMapFragment) getChildFragmentManager().findFragmentById(R.id.mapFragment);
    FragmentManager fm = getChildFragmentManager();

    if (mapFragment == null) {
        mapFragment = SupportMapFragment.newInstance();
        fm.beginTransaction().replace(R.id.mapFragment, mapFragment).commit();
        fm.executePendingTransactions();
    }

    mapFragment.getMapAsync(this);

    //Global PlaceAutocompleteFragment autocompleteFragment;


    if (autocompleteFragment == null) {
        autocompleteFragment = (PlaceAutocompleteFragment) getActivity().getFragmentManager().findFragmentById(R.id.place_autoCompleteFragment);

    }

在onDestroyView中清除SupportMapFragment和SupportMapFragment。
@Override
public void onDestroyView() {
    super.onDestroyView();


    if (getActivity() != null) {
        Log.e("res","place dlted");
        android.app.FragmentManager fragmentManager = getActivity().getFragmentManager();
        android.app.FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.remove(autocompleteFragment);
        fragmentTransaction.commit(); 
       //Use commitAllowingStateLoss() if getting exception 

        autocompleteFragment = null;
    }


}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接