一、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
不重复造轮子,引用自: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 的onCreate
和 onDestroy
方法不会被重复调用。所以在旋转屏 Fragment 中,我们经常会设置setRetainInstance(true)
,这样旋转时 Fragment 不需要重新创建。
如果你的 App 恰好可以不做转屏,那么你可以很省事的在 Manifest 文件中添加标注,强制所有页面使用竖屏/横屏。如果你的页面不幸的需要支持横竖屏切换,那么你在预估工作量或者给客户报价时一定要考虑到。虽然加入转屏支持不会导致工作量翻倍,但是却有可能引起许多问题。尤其当页面有很多业务逻辑,有状态值的时候。所以我们在项目开发过程中,应该知道什么时候需要考虑状态保存,当状态保存出现问题时,应该怎么解决之。
三、fragment间的通讯(由同一个activity托管下的两个fragment之间的通讯)
要传递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).
以上如有错误,同行有责任指正哦~,感激不尽