Android Programming 3rd Edition Chap 12 Challenge

Android编程权威指南 第3版 第12章 挑战练习。

挑战练习1 更多对话框

和 DatePickerFragment 几乎完全一样。

挑战练习2 实现响应式 DialogFragment

第一、二步就按提示完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// dialog_date.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<DatePicker
android:id="@+id/dialog_date_picker"
android:calendarViewShown="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</DatePicker>

<Button
android:id="@+id/date_ok_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/button_ok"
android:layout_gravity="center_horizontal|bottom"/>

</FrameLayout>
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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// DatePickerFragment.java
public class DatePickerFragment extends DialogFragment {
public static final String ARG_DATE = "date";
public static final String EXTRA_DATE = "com.bignerdranch.android.criminalintent.date";
private static final String TAG = "DatePickerFragment";

private DatePicker mDatePicker;
private Button mOKButton;
private Date mDate;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 获取 CrimeFragment 传给 DatePickerActivity 的 date 数据
mDate = (Date) getActivity().getIntent().getSerializableExtra(EXTRA_DATE);
Log.d(TAG, "onCreate:" + mDate.toString());
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

Log.d(TAG, "onCreateView:" + mDate.toString());
Calendar calendar = Calendar.getInstance();
calendar.setTime(mDate);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);

View v = inflater.inflate(R.layout.dialog_date, container, false);

mDatePicker = v.findViewById(R.id.dialog_date_picker);
mDatePicker.init(year, month, day, null);
mOKButton = v.findViewById(R.id.date_ok_button);
mOKButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year, month, day, hour, minute, second).getTime();
sendResult(Activity.RESULT_OK, date);
}
});

return v;
}

// 给 CrimeFragment 返回用户修改的 date 数据
private void sendResult(int resultCode, Date date){
Intent intent = new Intent();
intent.putExtra(EXTRA_DATE, date);
getActivity().setResult(resultCode, intent);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// DatePickerActivity.java
public class DatePickerActivity extends SingleFragmentActivity{
private static final String EXTRA_DATE= "com.bignerdranch.android.criminalintent.date";

@Override
protected Fragment createFragment() {
return new DatePickerFragment();
}
// 用于 CrimeFragment 调用,传递 date 数据给 DatePickerFragment
public static Intent newIntent(Context packageContext, Date date){
Intent intent = new Intent(packageContext, DatePickerActivity.class);
intent.putExtra(EXTRA_DATE, date);
return intent;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// CrimeFragment.java
mDateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 修改启动方式
Intent intent = DatePickerActivity.newIntent(getActivity(), mCrime.getDate());
startActivityForResult(intent, REQUEST_DATE);
}
});

...

// onActivityResult 无需修改
@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);
mCrime.setDate(date);
updateDate();
}
}

第三步 涉及 第17章

按17章的步骤修改代码,不考虑新建Crime和数据库部分,可使 CrimeList 和 Crime明细 在平板上显示在屏幕两侧。

但是第二步把 DatePicker 托管到 Activity 了,现在启动DatePicker 都是打开一个 Activity 充满屏幕,所以改成判断设备是手机还是平板,决定启动 DatePickerFragment 的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// CrimeFragment.java
mDateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 如果是手机启动Activity, 若是平板启动对话框 (参考第17章
if(getActivity().findViewById(R.id.detail_fragment_container) == null){
Intent intent = DatePickerActivity.newIntent(getActivity(), mCrime.getDate());
startActivityForResult(intent, REQUEST_DATE);
}else {
FragmentManager manager = getFragmentManager();
DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE);
dialog.show(manager, DIALOG_DATE);
}
}
});

现在在平板上就会显示对话框了,但是按 DatePicker 的OK按钮后,CrimeFragment 的日期却不更新。

通过 Logcat 发现,sendResult() 里发送的就是原来的date,怎么回事呢?

之前自己为了方便,实现第二步的时候,用新建 Activity 的方式启动 DatePicker 这个页面,我直接把 onCreateDialog() 里的代码 copy 到了 onCreateView() 里,只改动了 这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// DatePickerFragment.java -- onCreateView()
mDatePicker = v.findViewById(R.id.dialog_date_picker);
mDatePicker.init(year, month, day, null);
mOKButton = v.findViewById(R.id.date_ok_button);
mOKButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year, month, day, hour, minute, second).getTime();
sendResult(Activity.RESULT_OK, date);
}
});

而在onCreateDialog里的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// DatePickerFragment.java -- onCreateDialog()
mDatePicker = v.findViewById(R.id.dialog_date_picker);
mDatePicker.init(year, month, day, null);
return new AlertDialog.Builder(getActivity())
.setView(v)
.setTitle(R.string.time_picker_title)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year, month, day, mHour, mMinute, mSecond).getTime();
sendResult(Activity.RESULT_OK, date);
}
})
.create();

通过在生命周期函数里打 log 发现如下生命周期调用顺序:

平板

平板

手机

手机

onCreateView() 在两种方式启动时都会调用,而手机启动 Activity时不会调用 onCreateDialog() 方法。

所以在平板上没返回修改后的 date 应该是mDatePicker init了两次导致的,所以在 onCreateView() 里做一个判断,如果 mDatePicker不为空的话,再对其进行 findViewById 和 init:

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
// DatePickerFragment.java
public class DatePickerFragment extends DialogFragment {
public static final String ARG_DATE = "date";
public static final String EXTRA_DATE = "com.bignerdranch.android.criminalintent.date";
public static final String TAG = "DatePickerFragment";

private DatePicker mDatePicker;
private Button mOKButton;
private Date mDate;
private int mYear;
private int mMonth;
private int mDay;
private int mHour;
private int mMinute;
private int mSecond;

// 新建对话框 Fragment 时用 arguments 传递信息
public static DatePickerFragment newInstance(Date date){
Bundle args = new Bundle();
args.putSerializable(DatePickerFragment.ARG_DATE, date);

DatePickerFragment fragment = new DatePickerFragment();
fragment.setArguments(args);
return fragment;
}

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

Log.d(TAG, "onCreate called");
// Activity 方式启动时获取 date 信息
Date date = (Date) getActivity().getIntent().getSerializableExtra(EXTRA_DATE);
if(date != null){
mDate = date;
Log.d(TAG, "intent:" + mDate);
}
// 对话框方式启动时获取 date 信息
if(getArguments() != null){
mDate = (Date) getArguments().getSerializable(DatePickerFragment.ARG_DATE);
Log.d(TAG, "arguments:" + mDate.toString());
}

Calendar calendar = Calendar.getInstance();
calendar.setTime(mDate);
mYear = calendar.get(Calendar.YEAR);
mMonth = calendar.get(Calendar.MONTH);
mDay = calendar.get(Calendar.DAY_OF_MONTH);
mHour = calendar.get(Calendar.HOUR_OF_DAY);
mMinute = calendar.get(Calendar.MINUTE);
mSecond = calendar.get(Calendar.SECOND);

}

// 启动 Activity 时不会调用该方法
@NonNull
@Override
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateDialog");

View v = LayoutInflater.from(getActivity())
.inflate(R.layout.dialog_date, null);

// 对话框方式不显示多余的 OK 按钮
mOKButton = v.findViewById(R.id.date_ok_button);
mOKButton.setVisibility(View.INVISIBLE);

mDatePicker = v.findViewById(R.id.dialog_date_picker);
mDatePicker.init(mYear, mMonth, mDay, null);
return new AlertDialog.Builder(getActivity())
.setView(v)
.setTitle(R.string.time_picker_title)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year, month, day, mHour, mMinute, mSecond).getTime();
sendResult(Activity.RESULT_OK, date);
}
})
.create();
}

// 创建 Activity 时调用
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.d(TAG, "onCreateView called");

View v = inflater.inflate(R.layout.dialog_date, container, false);

// 避免因重新 init 而使在平板上不能返回修改后的 date 给 CrimeFragment
if(mDatePicker == null){
mDatePicker = v.findViewById(R.id.dialog_date_picker);
mDatePicker.init(mYear, mMonth, mDay, null);
}
mOKButton = v.findViewById(R.id.date_ok_button);
mOKButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year, month, day, mHour, mMinute, mSecond).getTime();
sendResult(Activity.RESULT_OK, date);
getActivity().finish();// 按确定后直接返回上一界面--CrimeFragment
}
});

return v;
}

// 返回date给CrimeFragment,要判断两种方式
private void sendResult(int resultCode, Date date){
Intent intent = new Intent();
intent.putExtra(EXTRA_DATE, date);
Log.d(CrimeFragment.TAG, "sendDate:"+ date.toString());
if(getTargetFragment() == null){
getActivity().setResult(resultCode, intent);
}else{
getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, intent);
}
}
}

最终效果:

平板设备显示对话框

平板设备显示对话框

手机设备显示全屏 Activity

手机设备显示全屏 Activity