Fragment的进一步使用(三)--- 关于DialogFragment对话框,设备旋转与fragment,fragment间的通讯 , fragment的Menu

一、DialogFragment对话框的使用

提起android的对话框,更多想到的是AlertDialog,但是android开发原则更多推荐使用DialogFragment,通过FragmentManager来管理,以致可以使用更多的配置选项来显示对话框。

例如:独立配置使用AlertDialog会在设备旋转后消失,这当然不是我们所希望的,但是DialogFragment封装的AlertDialog则不会出现这个问题。

实现过程:

         1、创建DialogFragment

         2、创建AlertDialog

         3、通过FragmentManager在屏幕上显示对话框


1)创建DatePickerFragment 继承DialogFragment,对话框里面包含一个时间选择器DatePicker

package com.example.learn_fragment.fragment;

import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.AlertDialog;
import android.view.View;
import android.widget.DatePicker;

import com.example.learn_fragment.R;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

/**
 * Created by Administrator on 2016/8/1.
 */

public class DatePickerFragment extends android.support.v4.app.DialogFragment{

    public static final String EXTRA_DATE = "criminalintent.date";
    private Date mDate;

    // fragment与activity通讯的较好方式之一
    public static DatePickerFragment newInstance(Date date){
        Bundle args = new Bundle();
        args.putSerializable(EXTRA_DATE, date);
        DatePickerFragment fragment = new DatePickerFragment();
        fragment.setArguments(args);
        return fragment;
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {

        // Arguments的方法可以应对设备旋转,新实例可以从Arguments中获取已在onDateChanged中保存进Arguments的mDate实例
        // 相比onSaveInstanceState保存DatePickerFragment数据更方便简单
        // 从另一方面来说,直接保存该DialogFragment更方面,但是DialogFragment在保存实例还存在bug,所以还是通过Arguments来保存较好
        mDate = (Date) getArguments().getSerializable(EXTRA_DATE);
        Calendar calendar = Calendar.getInstance();
        calendar.setTime(mDate);
        int year = calendar.get(Calendar.YEAR);
        final int month = calendar.get(Calendar.MONTH);
        final int day = calendar.get(Calendar.DAY_OF_MONTH);

//--------对话框内容
       View v = getActivity().getLayoutInflater().inflate(R.layout.dialog_date, null);

        DatePicker datePicker = (DatePicker) v.findViewById(R.id.dialog_date_datePicker);
        datePicker.init(year, month, day, new DatePicker.OnDateChangedListener() {
            @Override
            public void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
                mDate = new GregorianCalendar(year, monthOfYear, dayOfMonth).getTime();
                getArguments().putSerializable(EXTRA_DATE, mDate);
            }
        });

        return new AlertDialog.Builder(getActivity())
                .setView(v)
                .setTitle(R.string.date_picker_title)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        sendResult(Activity.RESULT_OK);
                    }
                })
                .create();
//--------对话框内容

 } /** * 将日期回传给目标的Fragment(CrimeFragment) * @param resultCode */ private void sendResult(int resultCode){ if(getTargetFragment() == null){ return; } Intent i = new Intent(); i.putExtra(EXTRA_DATE, mDate); getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i); }}
 
  

dialog_date.xml




    



2)创建AlertDialog,已经在return那里创建了。

3)通过FragmentManager在屏幕上显示对话框

和其他fragment一样,DialogFragment实例也是由托管activity的FragmentManager管理着的。要将DialogFragment添加给FragmentManager管理并放到屏幕上,可调用fragment实例一下方法:

public void show(FragmentManager manager, String tag)

public void show(FragmentTransaction transaction, String tag)

string 参数可以唯一识别FragmentManager队列中的DialogFragment。可以按照需要选择究竟是使用FragmentManager还是FragmentTransaction。如果传入FragmentManager参数,则事物可以自动创建并提交。这里选用传入FragmentManager参数。

通过一个按钮触发对话框:

mDateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fm = getSupportFragmentManager();
                DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
                // 把CrimeFragment设置为DatePickerFragment的目标Fragment,这样就可以把数据从DatePickerFragment传回目标Fragment
                dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
                // 展示dialog(DialogFragment),也将dialog交由fm管理,fragment会自动提交
                dialog.show(fm, DIALOG_DATE);
            }
        });

效果图:

设备需旋转不会消失的dialog

Fragment的进一步使用(三)--- 关于DialogFragment对话框,设备旋转与fragment,fragment间的通讯 , fragment的Menu_第1张图片Fragment的进一步使用(三)--- 关于DialogFragment对话框,设备旋转与fragment,fragment间的通讯 , fragment的Menu_第2张图片



二、设备旋转与fragment

不重复造轮子,引用自:http://www.gongmingqm10.net/blog/2015/12/16/you-should-know-about-android-rotate/

在 Android 开发或面试过程中,屏幕旋转是一个容易让人忽视的知识点。在我之前经历的项目中,App 通常是为竖屏状态设置的,所以通常我们会对每个页面都设置竖屏方向,这时候我们不需要考虑旋转屏问题。但是最近项目中,我们的 App 是为平板设计的,而横竖屏旋转是属于客户的一个需求,当然平板上横竖屏的确比较常用。所以就借此机会研究了下 Android 横竖屏问题。

横竖屏之所以需要引起开发者的注意,是因为 App 在横竖屏切换的过程中,页面会重绘,那么页面上已有的数据(比如登录页面已经输入的用户名)如何保存成为了一个问题。按照官方的推荐,Activity本身的确有处理旋转屏事件的函数,但是当一个页面中需要保存的数据很多的时候(比如很多 EditText),还是手工处理,就显得有些繁琐了。下面我们将循序渐进地探索 Android 屏幕旋转之最佳实践。

Activity 旋转中的保存与恢复

在解决问题或者探索最佳实践的时候,我们可以从简单的问题入手,慢慢衍生至复杂的情形,最后再抽象出一些比较通用的解决方案。这一步我们单纯的探索一个 Activity(没有 Fragment)的数据保存与恢复。

Activity 的生命周期

Activity 从创建到呈现在用户面前到消亡,有着自己完整的生命周期:

旋转屏幕时,打印相关 Log 如下:

从 Log 可以看到,在屏幕旋转时,原来的 Activity 调用了 onDestroy,随后重新实例化了一个 MainActivity。重新实例化的 MainActivity 也会经历 “onCreate –> onStart –> onResume” 的生命周期。

Activity 中窗口保存与恢复

为了进一步探索 Activity 在旋转屏过程中的数据保存及恢复的逻辑,我们构造了一个具有用户名和密码的登录界面。当用户在用户名中输入用户名时,这时候旋转屏幕,此时期望的操作应该是输入的用户名仍然能够保留:

在不添加任何额外代码的情况下,我们可以看到输入框中的数据在旋转屏后仍然能够保留,这些控件基本状态的值是如何被保留的呢?

Activity 中方法 protected void onSaveInstanceState(Bundle outState) {...} 主要做状态保存相关的处理,如果我们有需要特地保存的变量等,我们可以在onSaveInstanceState 中保存,保存后的 bundle 以 outState Bundle 的格式保存。当 Activity 再次被初始化时,onCreate(Bundle savedInstanceState) 会将保存的 bundle 传递给 Activity 主页面,Activity 主页面接收到这些状态保存的数据后,能够根据保存中的控件的ID信息,状态数据等对页面进行自动的初始化。当我们转屏时,会主动触发onSaveInstanceState 被调用。Log 打印如下:

根据 Log 很明确看出在旋转屏之后,随着 onPause 的执行,onSaveInstanceState 也被执行。当 Activity 再次初始化时,onCreate(Bundle savedInstanceState) 会传递回一个非空的 savedInstanceState(而当 Activity 第一次初始化时此值为空),同时onRestoreInstanceState 也会被调用,用来将保存的窗口状态信息重新应用:

生命周期:onCreate –> onStart –> onResume –> Running 转屏 –>onPause –> onSaveInstanceState –> onStop –>onDestroy –> onCreate –> onStart –> onRestoreInstanceState –> onResume;

通过对 savedInstanceState 的查看,发现 savedInstanceState 中包含有 Key 为 android:viewHierarchyState 的 bundle 数据,此 bundle 数据具体内容如下:

1
2
3
4
5
Bundle[{android:views={
      16908290=android.view.AbsSavedState$1@347440f8,         2131492927=android.view.AbsSavedState$1@347440f8,       2131492928=android.view.AbsSavedState$1@347440f8,       2131492929=android.support.v7.widget.Toolbar$SavedState@391b69d1,       2131492930=android.view.AbsSavedState$1@347440f8,       2131492944=TextView.SavedState{138fec36 start=8 end=8 text=gongming},       2131492945=TextView.SavedState{16043737 start=0 end=0 text=},       2131492946=android.view.AbsSavedState$1@347440f8
  },
  android:focusedViewId=2131492944}
]

可以看到 username 的输入框 EditText,其 ID 以及当前的值都已经保存至 savedInstanceState 中。

那么,对于 Activity.java 的而言,最重要的保存数据和恢复数据的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";

protected void onSaveInstanceState(Bundle outState) {
    outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
    Parcelable p = mFragments.saveAllState();
    if (p != null) {
        outState.putParcelable(FRAGMENTS_TAG, p);
    }
    getApplication().dispatchActivitySaveInstanceState(this, outState);
}

protected void onRestoreInstanceState(Bundle savedInstanceState) {
    if (mWindow != null) {
        Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
        if (windowState != null) {
            mWindow.restoreHierarchyState(windowState);
        }
    }
}

这就是为什么我们没有做过多的处理却可以让 App 在旋转屏幕时仍然自动保存恢复输入框中的文字。

Activity 中数据保存与恢复

既然 Activity 本身对窗口(控件)的状态信息进行了保存及恢复处理。那么我们在屏幕切换时最应该关心的就是页面数据的保存与恢复。页面数据主要有两种:

  • API 请求的数据:在横竖屏切换时对API数据进行保存及恢复能够防止 API 的重复调用;
  • 页面中的状态值:在 Activity 运行过程中被改变的状态值,在恢复时需要手动保存及恢复,以便不影响状态值相关的页面逻辑;

在 Activity 中进行变量的存储及读取时,使用 onSaveInstanceState 进行数据的存储,使用 onRestoreInstanceState 进行数据的读取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private boolean shouldSaveData = false;

private static final String KEY_SHOULD_SAVE_DATA = "save_data_key";

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putBoolean(KEY_SHOULD_SAVE_DATA, shouldSaveData);
    super.onSaveInstanceState(outState);

    Log.i(TAG, "@@Activity onSaveInstanceState@@" + this.toString());
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    shouldSaveData = savedInstanceState.getBoolean(KEY_SHOULD_SAVE_DATA);
    Log.i(TAG, "@@Activity onRestoreInstanceState@@" + this.toString());
}

Activity 中弹框的状态保存及恢复

在 Activity 中经常会遇到 Dialog,甚至像下面具有输入框的 Dialog:

当用户点击 Login 按钮,弹出弹框需要用户签名,这时如果屏幕不小心进行了一次旋转,那么这个弹出的 Dialog 便消失,随之消失的还有用户输入了一半的输入框文字。Activity 在旋转过程中对于 Dialog 自身的生命周期进行很好的管理,如果为了达到更好的用户体验,转屏时也需要保存输入框状态,那么此处我们强烈推荐用户使用DialogFragment 代替 Dialog。

由于 Fragment 也具有生命周期,使用 DialogFragment 之后,我们结合 Activity 与 Fragment 的生命周期,查看整个过程经历了哪些流程。

结合 DialogFragment 在 Activity 中旋转的重新初始化及数据恢复,我们可以看到执行顺序如下:

在转屏时,Activity 和 Fragment 都会重新实例化,并且都通过 onSaveInstanceState 进行状态保存。值得注意的是不同于 Activity, Fragment 并没有onRestoreInstanceState 方法,Fragment 的状态恢复在 onActivityCreated 方法中。查看 DialogFragment 源码,我们可以看到如下调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
  ...
    if (savedInstanceState != null) {
        Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG);
        if (dialogState != null) {
            mDialog.onRestoreInstanceState(dialogState);
        }
    }
}

...

@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    if (mDialog != null) {
        Bundle dialogState = mDialog.onSaveInstanceState();
        if (dialogState != null) {
            outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState);
        }
    }
  ...
}

由于 DialogFragment 其实就是展示一个 Dialog,而 DialogFragment 对 Dialog 的状态保存及恢复使得 Dialog 的状态得以保存。

Fragment 状态的保存及恢复

有了上面对 DialogFragment 转屏时状态保存及恢复的研究,那么在一个普通的 Fragment(DialogFragment 是一种特殊的 Fragment) 中状态保存及恢复又是怎样的呢?

实际上通过 DialogFragment 我们可以知道保存状态值还是通过 onSaveInstanceState 方法,而 onActivityCreated 中则可以获取状态值。

在转屏时,我们会有很多特殊的考虑。所以如果你的 App 需要支持横竖屏切换,你可以留意如下几点:

1. Dialog 转屏消失问题

Dialog 转屏消失在现实中是一个很常见的情形,对应的解决方案就是利用 DialogFragment 来替代 Dialog。这样旋转屏幕时弹起的 Dialog 就不会消失。

2. Fragment 保存组件信息的坑

最近在项目中发现,有时候放置在 Fragment 中的 ListView 转屏后不能自动回到转屏之前的位置。后来发现导致原因是 Activity 的 Layout 在添加 Fragment 时候没有指定 id 或 tag。于是该 Fragment 在 Activity 重绘时不能被系统当作 “同一个” Fragment,所以旋转时控件的一些基本状态信息没办法恢复。

1
2
3
4
5
6
7
8
9

    android:id="@+id/home_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:name="net.gongmingqm10.androidrotate.HomeFragment" />

其中的 android:id="@+id/home_fragment" 是重点。一旦 Fragment 的状态保存出现问题,可先确认 Fragment 是不是设置了 id 或 tag。

3. 使用 setRetainInstance

关于 Fragment,我们发现 setRetainInstance 方法经常被用到,那么这个方法的作用是什么呢?我们看看官方的解释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
 * Control whether a fragment instance is retained across Activity
 * re-creation (such as from a configuration change).  This can only
 * be used with fragments not in the back stack.  If set, the fragment
 * lifecycle will be slightly different when an activity is recreated:
 * 
    *
  • {@link #onDestroy()} will not be called (but {@link #onDetach()} still * will be, because the fragment is being detached from its current activity). *
  • {@link #onCreate(Bundle)} will not be called since the fragment * is not being re-created. *
  • {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} will * still be called. *
*/

结合方法名以及方法的解释,可以知道一旦我们设置 setRetainInstance(true),意味着在 Activity 重绘时,我们的 Fragment 不会被重复绘制,也就是它会被“保留”。为了验证其作用,我们发现在设置为true 状态时,旋转屏幕,Fragment 依然是之前的 Fragment。而如果将它设置为默认的 false,那么旋转屏幕时 Fragment 会被销毁,然后重新创建出另外一个 fragment 实例。并且如官方所说,如果 Fragment 不重复创建,意味着 Fragment 的onCreateonDestroy 方法不会被重复调用。所以在旋转屏 Fragment 中,我们经常会设置setRetainInstance(true),这样旋转时 Fragment 不需要重新创建。

如果你的 App 恰好可以不做转屏,那么你可以很省事的在 Manifest 文件中添加标注,强制所有页面使用竖屏/横屏。如果你的页面不幸的需要支持横竖屏切换,那么你在预估工作量或者给客户报价时一定要考虑到。虽然加入转屏支持不会导致工作量翻倍,但是却有可能引起许多问题。尤其当页面有很多业务逻辑,有状态值的时候。所以我们在项目开发过程中,应该知道什么时候需要考虑状态保存,当状态保存出现问题时,应该怎么解决之。


三、fragment间的通讯(由同一个activity托管下的两个fragment之间的通讯)

Fragment的进一步使用(三)--- 关于DialogFragment对话框,设备旋转与fragment,fragment间的通讯 , fragment的Menu_第3张图片


要传递CrimeFragment的记录日期到DatePickerFragment,将这个日期作为DatePickerFragment的初始化日期。有一种简单较好的方法是在DatePickerFragment实现一个newInstance()方法 , (DatePickerFragment.java)

    public static DatePickerFragment newInstance(Date date){
        Bundle args = new Bundle();
        args.putSerializable(EXTRA_DATE, date);
        DatePickerFragment fragment = new DatePickerFragment();
        fragment.setArguments(args);
        return fragment;
    }

然后将Date作为argument,在DatePickerFragment创建时附加给DatePickerFragment


回传日期:从DatePickerFragment回传给CrimeFragment

1、设置目标fragment

类似于activity间的关联,可将CrimeFragment设置为DatePickerFragment的目标fragment。调用一下方法:

public void setTargetFragment(Fragmetn fragment, int requestCode)

该方法接受目标fragment以及一个识别请求码。目标fragment可使用该识别请求码通知是哪一个fragment在返回数据信息。

目标fragment以及识别请求码有FragmentManager负责跟踪记录,我们可以调用fragment(设置目标fragment的fragment)的getTargetFragment() 和 getTargetRequestCode() 方法获取它们。

CrimeFragment.java

这是由一个按钮启动的DatePickerFragment

      mDateButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                FragmentManager fm = getActivity().getSupportFragmentManager();
                DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
                // 把CrimeFragment设置为DatePickerFragment的目标Fragment,这样就可以把数据从DatePickerFragment传回目标Fragment
                dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
                // 展示dialog(DialogFragment),也将dialog交由fm管理,fragment会自动提交
                dialog.show(fm, DIALOG_DATE);
            }
        });

2、传递数据给目标fragment

建立CrimeFragment与DatePickerFragment间的联系后,需将日期数据返回给CrimeFragment。返回的日期数据将作为extra附加给Intent。

使用的是DatePickerFragment的类方法将intent传入CrimeFragment.onActivityResult(int , int , Intent)方法。

(不要忘记这两个fragment是由同一个activity托管的)

Activity.onActivityResult(...)方法是ActivityManager在子activity销毁后调用的父activity方法。处理activity间的数据返回时,无需亲自动手,ActivityManager会自动调用Activity.onActivityResult(...)方法。父activity接收到Activity.onActivityResult(...)方法的调用后,其FragmentManager会调用对应fragment的Fragment.onActivityResult(...)方法。

所以,在处理由同一个activity托管的两个fragment间的数据返回时,可借用Fragment.onActivityResult(...)方法。因此,直接调用目标fragment的Fragment.onActivityResult(...)方法,即可实现数据的回传。该方法有我们需要的信息:

 1)、一个与setTargetFragment(...)方法匹配的识别请求码,用以告知目标fragment返回结果来自哪里。

 2)、一个决定下一步该采取什么行动的结果代码

 3)、一个含有extra数据信息的Intent

在DatePickerFragment新建一个方法:

    /**
     * 将日期回传给目标的Fragment(CrimeFragment)
     * @param resultCode
     */
    private void sendResult(int resultCode){
        if(getTargetFragment() == null){
            return;
        }
        Intent i = new Intent();
        i.putExtra(EXTRA_DATE, mDate);
        getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i);
    }

调用该方法的时机当然是在选好日期后按确定按钮的时候:

        return new AlertDialog.Builder(getActivity())
                .setView(v)
                .setTitle(R.string.date_picker_title)
                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        sendResult(Activity.RESULT_OK);
                    }
                })
                .create();

响应DatePickerFragment对话框(CrimeFragment.java):

    /**
     * 处理DataPickerFragment回传的数据
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if(resultCode != Activity.RESULT_OK)
            return;
        if(requestCode == REQUEST_DATE){
            Date date = (Date) data.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
            SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateString = formatter.format(date);
            mCrime.setDate(date);
            mDateButton.setText(dateString);
        }
    }


四、Fragment的Menu(右上角的菜单选项)

Activity类提供了管理选项菜单的回调函数onCreateOptionsMenu(Menu)方法

Fragment也有自己的一套选项菜单回调函数:

public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)

public boolean onOptionsItemSelected(MenuItem item)
使用方法跟activity的相差无几,也是配合menu.xml来使用。不过,Fragment的onCreateOptionsMenu(Menu, MenuInflater)方法是由FragmentManager负责调用的。因此,当activity接收到来自操作系统的onCreateOptionsMenu(...)方法回调请求时,我们必须明确告诉FragmentManager:其管理的fragment应接收onCreateOptionsMenu(...)方法的调用指令。要通知FragmentManager,需要调用一下方法:

public void setHasOptionsMenu(boolean hasMenu)

并且是在fragment的onCreate(...)方法中调用,setHasOptionsMenu(true).


以上如有错误,同行有责任指正哦~,感激不尽




你可能感兴趣的:(android)