我所见的Fragment
PolHislop
8年前
<h3>是什么</h3> <p>Fragment (碎片)是 Android 中的行为或用户界面部分。</p> <h3>为什么</h3> <p>在 Android 中引入 Fragment 主要是为了给大屏幕 (如平板电脑) 上更加动态和灵活的UI设计提供支持。使用Fragment可以在app适配平板时无需大范围的更改布局。下图是google提供的一个示例图:</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/e63c77992bb8aebe7396d17651065d86.png"></p> <p>如上两幅图中可以看出, 通过使用两个 Fragment 进行不同的排列组合可以很好的适配平板和手机两种不同布局风格, 而减少大量的布局开发作业.</p> <h3>时间点</h3> <p>Android 在 Android 3.0(API 级别 11)中引入了片段, 如果使用v4包可以兼容到 Android 1.6;</p> <p>如果使用 V4 包的话需要注意一些地方:</p> <p>1. 果你使用了v4包下的 Fragment , 那么所在的那个Activity就要继承 FragmentActivity .</p> <p>2. 如果要使用FragmentManager必须使用 getSupportFragmentManager() ;</p> <h3>怎么用</h3> <p>1. 生命周期</p> <p style="text-align:center"><img src="https://simg.open-open.com/show/91b2bb0dc9207898818291854066dca2.png"></p> <p>管理 Fragment 的生命周期与管理 Activity 的生命周期非常相似,片段也以三种状态存在:</p> <pre> <code class="language-java">1. 继续 片段在运行中的 Activity 中可见。 2. 暂停 另一个 Activity 位于前台并具有焦点,但此片段所在的 Activity 仍然可见(前台 Activity 部分透明,或未覆盖整个屏幕)。 3. 停止 片段不可见。宿主 Activity 已停止,或片段已从 Activity 中移除,但已添加到返回栈。 停止片段仍然处于活动状态(系统会保留所有状态和成员信息)。 不过,它对用户不再可见,如果 Activity 被终止,它也会被终止。</code></pre> <p>Fragment 与 Activity 的生命周期最显著的区别是它们各自返回栈中的存储方式, 和 Activity 停止时自动放入由系统管理的 Activity 返回栈不同, Fragment 仅在被移除的事务执行期间调用 addToBackStack() 显示请求保存实例时, 系统才会将 Fragment 放入由宿主 Activity 管理的返回栈.</p> <p>Fragment 与 Activity 的生命周期具有协调一致性, 这体现在 Activity 的每次生命周期回调都会引发每个片段的类似回调. 例如, 当 Activity 收到 onPause() 时, Activity 中的每个片段也会收到 onPause(). Fragment中还有一些额外的方法:</p> <pre> <code class="language-java">onAttach() 在片段已与 Activity 关联时调用(Activity 传递到此方法内)。 onCreateView() 调用它可创建与片段关联的视图层次结构。 onActivityCreated() 在 Activity 的 onCreate() 方法已返回时调用。 onDestroyView() 在移除与片段关联的视图层次结构时调用。 onDetach() 在取消片段与 Activity 的关联时调用。</code></pre> <p>使用 <fragment> 标签引入 Fragment 的 Activity 的创建到销毁, 生命周期方法调用的顺序:</p> <pre> <code class="language-java">-Fragment: onCreate -Fragment: onCreateView -Fragment: onViewCreated -Activity: onCreate -Fragment: onActivityCreated -Activity: onStart -Fragment: onStart -Activity: onResume -Fragment: onResume -Fragment: onPause -Activity: onPause -Fragment: onStop -Activity: onStop -Fragment: onDestroyView -Fragment: onDestroy -Fragment: onDetach -Activity: onDestroy</code></pre> <p>2. 管理 Fragment</p> <p>要想管理 Activity 中的 Fragment, 需要使用 FragmentManager. 可以从 Activity 中调用 getFragmentManager() 获取.</p> <p>可以使用 FragmentManager 执行的操作包括:</p> <ul> <li>通过 findFragmentById()(对于在 Activity 布局中提供 UI 的片段)或 findFragmentByTag()(对于提供或不提供 UI 的片段)获取 Activity 中存在的片段。</li> <li>通过 popBackStack()(模拟用户发出的返回命令)将片段从返回栈中弹出。</li> <li>通过 addOnBackStackChangedListener() 注册一个侦听返回栈变化的侦听器。</li> </ul> <p>注意: 在 Fragment 的嵌套情况下在父 Fragment 中使用 getFragmentManager() / getSupportFragmentManager() 获取 FragmentManager; 在子 Fragment 中需要使用getChildFragmentManager 来获取.</p> <p>2. Fragment的简单使用</p> <ul> <li>使用< fragment >标签在布局中添加碎片</li> <li>通过 android:name 属性来显式指明要添加的碎片类名,注意一定要将类的包名也加上。</li> </ul> <pre> <code class="language-java"><fragment android:id = "@+id/left_fragment" android:name = "com.example.FragmentTest.fragment.HomeFragment" android:layout_width = "1dp" android:layout_height = "match_parent" android:layout_weight = "1" tools:layout = "@layout/left_fragment"/></code></pre> <p>注:每个片段都需要一个唯一的标识符,重启 Activity 时,系统可以使用该标识符来恢复片段(您也可以使用该标识符来捕获片段以执行某些事务,如将其移除)。 可以通过三种方式为片段提供 ID:</p> <p>- 为 android:id 属性提供唯一 ID。</p> <p>- 为 android:tag 属性提供唯一字符串。</p> <p>- 如果您未给以上两个属性提供值,系统会使用容器视图的 ID。</p> <p>3. 动态添加Fragment</p> <p>碎片真正的强大之处在于,它可以在程序运行时动态地添加到活动当中</p> <ol> <li>创建待添加的碎片实例。</li> <li>获取到 FragmentManager,在活动中可以直接调用 getFragmentManager()方法得到。</li> <li>开启一个事务,通过调用 beginTransaction()方法开启。</li> <li>向容器内加入碎片,可以使用 replace() 或 add() 方法实现,需要传入容器的 id 和待添加的碎<br> 片实例。</li> <li>提交事务,调用 commit()方法来完成。</li> </ol> <pre> <code class="language-java">FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.add(R.id.fg_main, new MainFragment()); ft.addToBackStack(null); ft.commit();</code></pre> <p>注意:</p> <p>1. 调用 commit() 不会立即执行事务,而是在 Activity 的 UI 线程(“主”线程)可以执行该操作时再安排其在线程上运行。不过,如有必要,您也可以从 UI 线程调用 executePendingTransactions() 以立即执行 commit() 提交的事务。通常不必这样做,除非其他线程中的作业依赖该事务。</p> <p>2. 您只能在 Activity 保存其状态(用户离开 Activity)之前使用 commit() 提交事务。如果您试图在该时间点后提交,则会引发异常。 这是因为如需恢复 Activity,则提交后的状态可能会丢失。 对于丢失提交无关紧要的情况,请使用 commitAllowingStateLoss()。</p> <p>4. Fragment 和 Activity 之间进行通讯</p> <ol> <li>在 Activity 中获取相应 Fragment 的实例, 可以通过 FragmentManager 使用 findFragmentById() 或 findFragmentByTag().</li> </ol> <pre> <code class="language-java">RightFragment rightFragment = (RightFragment) getFragmentManager().findFragmentById(R.id.right_fragment);</code></pre> <ol> <li>得到当前碎片相关联的活动实例</li> </ol> <pre> <code class="language-java">MainActivity activity = (MainActivity) getActivity();</code></pre> <ol> <li> <p>同时出现在 Activity 中的两个 Fragment 之间如何传递信息</p> <p>执行此操作的一个好方法是,在片段内定义一个回调接口,并要求宿主 Activity 实现它。 当 Activity 通过该接口收到回调时,可以根据需要与布局中的其他片段共享这些信息。</p> </li> </ol> <pre> <code class="language-java">public static class FragmentA extends ListFragment { ... // Container Activity must implement this interface public interface OnArticleSelectedListener { public void onArticleSelected(Uri articleUri); } @Override public void onAttach(Activity activity) { super.onAttach(activity); try { mListener = (OnArticleSelectedListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement OnArticleSelectedListener"); } } ... }</code></pre> <h3>遇到的坑</h3> <p>对于Fragment中的各种坑推荐大神的 Fragment全解析系列 , 以及大神所写的 Fragmentation库 , 以下就只写一些我自己遇到的问题了.</p> <p>1. getActivity() return null</p> <p>原因:</p> <p>大部分原因是由于在 Fragment 与 Activity 断开关系 (onDetach() 被调用) 后使用 getActivity() (e.g. 异步网络访问中onDetach() 被调用, 获取访问结果后调用 getActivity() 操作布局)</p> <p>解决:</p> <ol> <li>最好的办法就是我们应该避免在已经onDetach这种情况之后再去调用宿主Activity对象,比如取消这些异步任务;</li> <li>还可以在Fragment基类里设置一个Activity mActivity的全局变量,在onAttach(Activity activity)里赋值,使用mActivity代替getActivity(),保证Fragment即使在onDetach后,仍持有Activity的引用(有引起内存泄露的风险,但是异步任务没停止的情况下,本身就可能已内存泄漏,相比Crash,这种做法“安全”些),即:</li> </ol> <pre> <code class="language-java">protected Activity mActivity; @Override public void onAttach(Activity activity) { super.onAttach(activity); this.mActivity = activity; } /** * 如果你用了support 23的库,上面的方法会提示过时,有强迫症的小伙伴,可以用下面的方法代替 */ @Override public void onAttach(Context context) { super.onAttach(context); this.mActivity = (Activity)context; }</code></pre> <p>2. 异常:Can not perform this action after onSaveInstanceState</p> <p>原因:</p> <p>在你离开当前Activity等情况下,系统会调用onSaveInstanceState()帮你保存当前Activity的状态、数据等,直到再回到该Activity之前(onResume()之前),你使用commit()提交了Fragment事务,就会抛出该异常!</p> <p>解决: 1. (不推荐)该事务使用`commitAllowingStateLoss()`方法提交, 但是有可能导致该次提交无效!(在此次离开时恰巧Activity被强杀时) 2. (推荐)在重新回到该Activity的时候(onResumeFragments()或onPostResume()), 再执行该事务!</p> <h3>注意事项</h3> <p>参数传递</p> <p>对Fragment传递数据,建议使用`setArguments(Bundle args)`,而后在onCreate中使用`getArguments()`取出,在 “内存重启”前,系统会帮你保存数据,不会造成数据的丢失。和Activity的Intent恢复机制类似。</p> <p>创建 Fragment 对象</p> <p>使用`newInstance(参数) `创建Fragment对象,优点是调用者只需要关系传递的哪些数据,而无需关心传递数据的Key是什么。</p> <pre> <code class="language-java">public static class TestFragment extends Fragment { private static final String ARG = "arg"; public static Fragment newInstance(String arg) { TestFragment fragment = new TestFragment(); Bundle bundle = new Bundle(); bundle.putString( ARG, arg); fragment.setArguments(bundle); return fragment; } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout. fragment_main, container, false); TextView tv = (TextView) rootView.findViewById(R.id. tv); tv.setText(getArguments().getString( ARG)); return rootView; } }</code></pre> <p>add() 和 replace()</p> <ol> <li> <p>add() 和 replace() 的区别</p> <ul> <li>show(),hide()最终是让Fragment的View setVisibility(true还是false),不会调用生命周期;</li> <li>replace()的话会销毁视图,即调用onDestoryView、onCreateView等一系列生命周期;</li> </ul> </li> <li> <p>使用建议</p> <p>如果你有一个很高的概率会再次使用当前的Fragment,建议使用show(),hide(),可以提高性能。</p> </li> </ol> <p>在我使用Fragment过程中,大部分情况下都是用show(),hide(),而不是replace()。</p> <ol> <li> <p>onHiddenChanged()</p> <p>当使用add() + show(),hide()跳转新的Fragment时,旧的Fragment回调 onHiddenChanged() ,不会回调onStop()等生命周期方法,而新的Fragment在创建时是不会回调onHiddenChanged(),这点要切记。</p> </li> </ol> <p>注意:如果你的app有大量图片,这时更好的方式可能是replace,配合你的图片框架在Fragment视图销毁时,回收其图片所占的内存。</p> <p>使用 ViewPager + Fragment 时使用懒加载</p> <p>我们在做应用开发的时候,一个Activity里面可能会以viewpager(或其他容器)与多个Fragment来组合使用,而如果每个fragment都需要去加载数据,或从本地加载,或从网络加载,那么在这个activity刚创建的时候就变成需要初始化大量资源。这样的结果,我们当然不会满意。那么,能不能做到当切换到这个fragment的时候,它才去初始化(不同于View的初始化,这里指的是数据的加载)呢?</p> <ul> <li> <p>答案就在Fragment里的setUserVisibleHint这个方法里具体可查看 android API</p> <p>该方法用于告诉系统,这个Fragment的UI是否是可见的。所以我们只需要继承Fragment并重写该方法,即可实现在fragment可见时才进行数据加载操作,即Fragment的懒加载。</p> </li> <li> <p>为什么不使用ViewPager的预加载功能呢</p> <p>现在大体上放置ViewPager预加载的方法有两种:</p> <ol> <li> <p>在使用ViewPager嵌套Fragment的时候,由于VIewPager的几个Adapter的设置来说,都会有一定的预加载(默认是左右各一个Frament)。通过设置 setOffscreenPageLimit (int number) 来设置预加载的熟练,在V4包中,默认的预加载是1,即使你设置为0,也是不起作用的,设置的只能是大于1才会有效果的。我们需要通过更改V4包中的默认属性才可以。</p> </li> <li> <p>限制预加载,会出现滑动过程中卡顿现象。其实Fragment中防止预加载主要是防止数据的预加载,Fragment中的VIew预加载是有好处的,我们可以通过Fragment中的一个方法来达到预加载View 但是不加载数据,在Fragment显示的时候才去加载数据。</p> </li> </ol> </li> </ul> <pre> <code class="language-java">public abstract class LazyFragment extends Fragment { protected boolean isVisible; /** * 在这里实现Fragment数据的缓加载. * @param isVisibleToUser */ @Override public void setUserVisibleHint(boolean isVisibleToUser) { super.setUserVisibleHint(isVisibleToUser); if(getUserVisibleHint()) { isVisible = true; onVisible(); } else { isVisible = false; onInvisible(); } } protected void onVisible() { lazyLoad(); } protected abstract void lazyLoad(); protected void onInvisible(){} }</code></pre> <p>在LazyFragment,增加了三个方法,一个是onVisiable,即fragment被设置为可见时调用,一个是onInvisible,即fragment被设置为不可见时调用。另外再写了一个lazyLoad的抽象方法,该方法在onVisible里面调用。你可能会想,为什么不在getUserVisibleHint里面就直接调用呢?</p> <p>我这么写是为了代码的复用。因为在fragment中,我们还需要创建视图(onCreateView()方法),可能还需要在它不可见时就进行其他小量的初始化操作(比如初始化需要通过AIDL调用的远程服务)等。而setUserVisibleHint是在onCreateView之前调用的,那么在视图未初始化的时候,在lazyLoad当中就使用的话,就会有空指针的异常。而把lazyLoad抽离成一个方法,那么它的子类就可以这样做:</p> <pre> <code class="language-java">public class OpenResultFragment extends LazyFragment { // 标志位,标志已经初始化完成。 private boolean isPrepared; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(LOG_TAG, "onCreateView"); View view = inflater.inflate(R.layout.fragment_open_result, container, false); //XXX初始化view的各控件 isPrepared = true; //这里再一次调用lazyLoad()方法是因为setUserVisibleHint()是在onCreateView()方法之后调用的 lazyLoad(); return view; } @Override protected void lazyLoad() { if(!isPrepared || !isVisible) { return; } //填充各控件的数据 } }</code></pre> <p> </p> <p>来自:http://blog.csdn.net/ice_coffee_mzp/article/details/53583641</p> <p> </p>