首页 > 代码库 > 《Android权威编程指南(The Big Nerd Ranch Guide)(第二版)》12.4挑战练习

《Android权威编程指南(The Big Nerd Ranch Guide)(第二版)》12.4挑战练习

本书第12章是讲解Dialog。12.4挑战练习是在CriminalIntent项目中,再增加一个TimePickerFragment的对话框fragment。通过在CrimeFragment用户界面上添加的时间按钮,
弹出TimePickerFragment界面,允许用户使用TimePicker组件选择crime发生的具体时间。

 

我的修改思路是:

  1. 按照DatePickerFragment实现的步骤、方法实现实现TimePickerFragment;
  2. crime日期与时间是一个整体:
    1. DatePickerFragment仅可以调整:年月日,时分不变动;
    2. TimePickerFragment仅可以调整:时分,时分不变动;
    3. 故Activity切换时,交换数据(附加到Intent上的extra数据单元共用一个Date。

 

具体实现如下。请各位高手拍砖。

 

1、使用AppCompat兼容库

依据DatePickerFragment实现方式,仍使用AppCompat兼容库。它在实现DatePickerFragment时,已经添加到CriminalIntent项目中。

 

2、增加、更新资源文件

2.1)增加标题:“Time of crime:”

在项目中,res\values\strings.xml增加 <string name="time_picker_title">Time of crime:</string>

即:

1 <resources>
2     ... ...
3     
4     <string name="time_picker_title">Time of crime:</string>
5 </resources>

 

2.2)在CrimFragment界面上添加Time Button

在CrimFragment界面上显示Time Button,需要在项目中res\layout\fragment_crime.xml文件,增加下列代码:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout
 3     ... ...
 4 
 5     <Button
 6         ... ...
 7         />
 8 
 9     <Button
10         android:id="@+id/crime_time"
11         android:layout_width="match_parent"
12         android:layout_height="wrap_content"
13         android:layout_marginLeft="16dp"
14         android:layout_marginRight="16dp"
15         />
16 
17     <CheckBox
18         ... ...
19         />
20 
21 </LinearLayout>

 

2.3)为了保证设备旋转后仍然能正常显示

还需在项目的res\layout-land\fragment_crime.xml文件增加类似上面代码:

 1 <?xml version="1.0" encoding="utf-8"?>
 2 <LinearLayout
 3     ... ...
 4 
 5         <Button
 6             ... ...
 7             />
 8 
 9         <Button
10             android:id="@+id/crime_time"
11             android:layout_width="wrap_content"
12             android:layout_height="wrap_content"
13             android:layout_weight="1"/>
14 
15         <CheckBox
16             ... ...
17             />
18 
19     ... ...
20 
21 </LinearLayout>

 

3、创建新的TimePickerFragment

TimePickerFragment是DialogFragment的子类。然后,在TimePickerFragment中,创建并配置显示TimePicker组件的AlertDialog实例。TimePickerFragment同样由CrimePagerActivity托管。

 

3.1)使用android.support.v4.app.DialogFragment库

创建TimePickerFragment类,并设置其超类DialogFragment,由android.support.v4.app.DialogFragment库支持。

 

在TimePickerFragment.java中,重载DialogFragment类的onCreateDialog(Bundle savedInstanceState)方法。由托管activity的FragmentManager会调用它,在屏幕上显示DialogFragment。

 

onCreateDialog(Bundle savedInstanceState)方法的实现代码,创建一个带标题栏和OK按钮的AlertDialog,代码如下。注意:导入AlertDialog时,还是选择AppCompat库中的版本:android.support.v7.app.AlertDialog。

 

 1 public class TimePickerFragment extends DialogFragment {
 2 
 3     @Override
 4     public Dialog onCreateDialog(Bundle savedInstanceState) {
 5 
 6         return new AlertDialog.Builder(getActivity())
 7                 .setTitle(R.string.time_picker_title)
 8                 .setPositiveButton(android.R.string.ok, null)
 9                 .create();
10     }
11 }

 

这里类似DatePickerFragment,使用AlerDialog.Builder类,以Fluent Interface的方式创建AlertDialog实例。

 

3.2)显示TimeDialogFragment

同DatePickerFragment一样,TimeDialogFragment实例也是由托管activity的FragmentManager管理。使用fragment实例的public void show(FragmentManager manager, String tag)方法,将TimePickerFragment添加给FragmentManager管理并放置到屏幕上。

 

在CrimeFragment(CrimeFragment.java)中,也为TimePickerFragment增加一个tag常量:

1 private static final String DIALOG_TIME = "DialogTime";

 

然后,在onCreateView(...)方法中,添加点击时间按钮展现TimePickerFragment界面,实现mTimeButton按钮的OnClickListener监听器接口,代码:

 1 public class CrimeFragment extends Fragment {
 2 
 3         ... ...
 4         private static final String DIALOG_TIME = "DialogTime";
 5 
 6         ... ...
 7 
 8         mTimeButton = (Button) v.findViewById(R.id.crime_time);
 9         mTimeButton.setText(DateFormat.format("h:mm a", mCrime.getDate()));
10         mTimeButton.setOnClickListener(new View.OnClickListener() {
11             @Override
12             public void onClick(View v) {
13                 FragmentManager manager = getFragmentManager();
14                 TimePickerFragment timeDialog = new TimePickerFragment();
15                 timeDialog.show(manager, DIALOG_TIME);
16             }
17         });
18 
19         ... ...
20 }

 

这就可以显示:带标题(Time of crime:)和OK(确定)按钮的AlertDialog。

 

3.3)设置对话框的显示内容

同DatePickerFragment.

这时需要在TimePirckerFragment(TimePirckerFragment.java)中,使用AlertDialog.Builder的setView(...)方法, 添加TimePicker组件给AlertDialog对话框:

1 public AlertDialog.Builder setView(View view)

 

该方法配置对话框,实现在标题栏与按钮之间显示传入的View对象 —— TimePicker。要展示TimePicker,需要在项目工具窗口中,以TimePicker为根元素,创建名为dialog_time.xml的布局文件:

1 <?xml version="1.0" encoding="utf-8"?>
2 <TimePicker
3     xmlns:android="http://schemas.android.com/apk/res/android"
4     android:id="@+id/dialog_time_time_picker"
5     android:layout_width="match_parent"
6     android:layout_height="match_parent"
7     android:calendarViewShown="false">
8 </TimePicker>

 

同时在TimePickerFragment.onCreateDialog(...)方法中,实例化DatePicker视图并添加给对话框:

 1     @Override
 2     public Dialog onCreateDialog(Bundle savedInstanceState) {
 3 
 4         View v = LayoutInflater.from(getActivity())
 5                 .inflate(R.layout.dialog_time, null);
 6 
 7         return new AlertDialog.Builder(getActivity())
 8                 .setView(v)
 9                 .setTitle(R.string.time_picker_title)
10                 .setPositiveButton(android.R.string.ok, null)
11                 .create();
12     }

 

此时运行CriminalIntent,点击时间按钮,TimePicker就显示在对话框上。

 

4、数据传递

在书中例子中,DatePicker将修改的日期传递给CrimeFragment后,时间就被“清零”(时间回到0点),而时间按钮上的文字还是之前的时间。

 

我对DatePickerFragment进行修改,将原来

 1     @Override
 2     public Dialog onCreateDialog(Bundle savedInstanceState) {
 3         ... ...
 4         Calendar calendar = Calendar.getInstance();
 5         ... ...
 6 
 7         return new AlertDialog.Builder(getActivity())
 8                 ... ...
 9                 .setPositiveButton(android.R.string.ok,
10                         new DialogInterface.OnClickListener() {
11                             @Override
12                             public void onClick(DialogInterface dialog, int which) {
13                                 ... ...
14 
15                                 Date date = new GregorianCalendar(year, month, day).getTime();
16 
17                                 ... ...
18                             }
19                         }
20                 )
21                 .create();
22     }

 

改为:

 1     @Override
 2     public Dialog onCreateDialog(Bundle savedInstanceState) {
 3         ... ...
 4         final Calendar calendar = Calendar.getInstance();
 5         ... ...
 6 
 7         return new AlertDialog.Builder(getActivity())
 8                 ... ...
 9                 .setPositiveButton(android.R.string.ok,
10                         new DialogInterface.OnClickListener() {
11                             @Override
12                             public void onClick(DialogInterface dialog, int which) {
13                                 ... ...
14 
15                                 Date date = new GregorianCalendar(year, month, day,
16                                         calendar.get(Calendar.HOUR_OF_DAY),
17                                         calendar.get(Calendar.MINUTE),
18                                         calendar.get(Calendar.SECOND)).getTime();
19 
20                                 ... ...
21                             }
22                         }
23                 )
24                 .create();
25     }

 

由于在inline函数DialogInterface.OnClickListener()的onClick(...)使用到calendar,这样就要将calender定义改为final,即:

1 final Calendar calendar = Calendar.getInstance();

 

这样在DatePicker传递修改后的日期回CrimeFragment后,时间持不变。我认为时间的改变正是由TimePicker来完成。就是说 crime的日期(date)和时间(time)是相互关联的。基于这一思路,参考DatePickerFragment,进一步添加时间值的传递。

 

4.1)把crime记录的时间传递给TimePickerFragment

这就需要新建newInstance(Date)方法,然后将Date作为argument附加给fragment。

 

要新时间返回给CrimeFragment,且更新相应视图和模型层,这需将时间值打包为extra并附加到Intent上,然后调用CrimeFragment.onActivityResult(...)方法,并传入准备好的Intent参数。如前所属“crime的日期(date)和时间(time)是相互关联的”,Date类包含时间,这样时间extra就应该与DatePicker共用一个单元。

 

4.2)传递数据给TimePickerFragment

如前所述,应该用含有时间的crime Date值保存到TimePickerFragment的argument bundle中,TimePickerFragment使可直接获取到它。这样就使用DatePrickerFragment的ARG_DATE标记(tag)。为此要import ARG_DATE,即:

1 import static com.example.bigzhg.criminalintent.DatePickerFragment.ARG_DATE;

 

在TimePickerFragment.java中,添加newInstance(Date)方法,完成创建和设置fragment argument。

 1 public class TimePickerFragment extends DialogFragment {
 2 
 3     ... ...
 4 
 5     public static TimePickerFragment newInstance(Date date) {
 6         Bundle args = new Bundle();
 7         args.putSerializable(ARG_DATE, date);
 8 
 9         TimePickerFragment fragment = new TimePickerFragment();
10         fragment.setArguments(args);
11         return fragment;
12     }
13 
14     ... ...
15 }

 

再在CrimeFragment中,用TimePickerFragment.newInstance(Date)方法替换掉TimePickerFragment的构造方法:

 1     @Override
 2     public View onCreateView(
 3             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 4 
 5         ... ...
 6 
 7         mTimeButton.setOnClickListener(new View.OnClickListener() {
 8             @Override
 9             public void onClick(View v) {
10                 FragmentManager manager = getFragmentManager();
11 
12                 // TimePickerFragment timeDialog = new TimePickerFragment();
13                 TimePickerFragment timeDialog = TimePickerFragment
14                         .newInstance(mCrime.getDate());
15                 ... ...
16             }
17         });
18 
19     ... ...
20 
21     }

 

同DatePickerFragment一样,使用Date中的信息来初始化TimePicker对象。

 

在onCreateDialog(...)方法内,从argument中获取crime日期(如前所述)的对象Date对象,再创建一个Calendar对象,然后用Date对象配置它,再从Calendar对象中取回所需信息(时、分),来为TimePicker进行初始化:

 1 public class TimePickerFragment extends DialogFragment {
 2 
 3     private TimePicker mTimePicker;
 4 
 5     ... ...
 6 
 7     @Override
 8     public Dialog onCreateDialog(Bundle savedInstanceState) {
 9         Date date = (Date) getArguments().getSerializable(ARG_DATE);
10 
11         Calendar calendar = Calendar.getInstance();
12         calendar.setTime(date);
13 
14         int hour = calendar.get(Calendar.HOUR_OF_DAY);
15         int minute = calendar.get(Calendar.MINUTE);
16 
17         View v = LayoutInflater.from(getActivity())
18                 .inflate(R.layout.dialog_time, null);
19 
20         mTimePicker = (TimePicker) v.findViewById(R.id.dialog_time_time_picker);
21         mTimePicker.setIs24HourView(false);
22         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
23             mTimePicker.setHour(hour);
24             mTimePicker.setMinute(minute);
25         } else {
26             mTimePicker.setCurrentHour(hour);
27             mTimePicker.setCurrentMinute(minute);
28         }
29 
30         return new AlertDialog.Builder(getActivity())
31                 .setView(v)
32                 .setTitle(R.string.time_picker_title)
33                 .setPositiveButton(android.R.string.ok, null)
34                 .create();
35     }
36 
37     ... ...
38 
39 }

 

在onCreateDialog(...)方法内,设置TimePicker是上下午(非24小时)格式:

1 mTimePicker.setIs24HourView(false);

 

由于我用于调试的手机时Galaxy Note II,系统为Android 4.4.2。因setCurrentHour()和setCurrentMinute()已在新系统中不再使用,故增加SDK的版本判断:

1         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
2             mTimePicker.setHour(hour);
3             mTimePicker.setMinute(minute);
4         } else {
5             mTimePicker.setCurrentHour(hour);
6             mTimePicker.setCurrentMinute(minute);
7         }

 

4.3)返回时间数据给CrimeFragment

CrimeFragment接收TimePickerFragment返回的时间数据,ActivityManager 负责跟踪管理父 activity与子activity间的关系。回传数据后,子activity被销毁,而ActivityManager 知道接收数据的是哪个activity。

 

4.3.1)设置目标fragment

这就要将CrimeFragment设置成TimePickerFragment的目标fragment。即使是在CrimeFragment和TimePickerFragment被销毁和重建后,操作系统也会重新关联它们。调用以下Fragment方法可建立这种关联:

public void setTargetFragment(Fragment fragment, int requestCode)

 

在CrimeFragment.java中,增加时间请求代码常量:

1 private static final int REQUEST_TIME = 1;

 

然后将CrimeFragment设为TimePickerFragment实例的目标fragment:

1 timeDialog.setTargetFragment(CrimeFragment.this, REQUEST_TIME);

 

即:

 1 public class CrimeFragment extends Fragment {
 2 
 3     ... ...
 4     private static final int REQUEST_TIME = 1;
 5 
 6     ... ...
 7 
 8     @Override
 9     public View onCreateView(
10             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
11 
12         ... ...
13 
14         mTimeButton.setOnClickListener(new View.OnClickListener() {
15             @Override
16             public void onClick(View v) {
17                 ... ...
18 
19                 timeDialog.setTargetFragment(CrimeFragment.this, REQUEST_TIME);
20                 ... ...
21 
22             }
23         });
24 
25         ... ...
26 
27         return v;
28     }

 

4.3.2)传递时间数据给目标fragment

建立CrimeFragment与TimePickerFragment间的联系后,需将数据回传给CrimeFragment。回传时间将作为extra附加给Intent。

 

使用TimePickerFragment类调用CrimeFragment.onActivityResult(int 请求代码, int 结果代码, Intent)方法,实现时间数据的回传。

  • 请求代码:与传入setTargetFragment(...)方法相匹配,告诉目标fragment返回结果来自哪里。
  • 结果代码:决定下一步该采取什么行动。
  • Intent:包含extra数据。

 

类似DatePickerFragment类,在TimePickerFragment类中,新建sendResult(...)私有方法,创建intent并将时间数据与crime Date构成新的Date数据,作为extra附加到intent上。最后调用CrimeFragment.onActivityResult(...)方法。

 

再就是使用sendResult(...)私有方法。用户点按对话框中的positive(确定)按钮时,需要从TimePicker中获取时间值并回传给CrimeFragment。在onCreateDialog(...)方法中,修改setPositiveButton(...),将null参数改DialogInterface.OnClickListener,并实现DialogInterface.OnClickListener监听器接口。在监听器接口的onClick(...)方法中,获取时间并调用sendResult(...)方法。

 

这里crime日期和时间一个“整体”,故公用DatePickerFragment的EXTRA_DATE:

1 import static com.example.bigzhg.criminalintent.DatePickerFragment.EXTRA_DATE;

 

其相关代码:

 1 ... ...
 2 import static com.example.bigzhg.criminalintent.DatePickerFragment.EXTRA_DATE;
 3 
 4 
 5 public class TimePickerFragment extends DialogFragment {
 6 
 7     ... ...
 8 
 9 
10     @Override
11     public Dialog onCreateDialog(Bundle savedInstanceState) {
12         ... ...
13 
14         final int year = calendar.get(Calendar.YEAR);
15         final int month = calendar.get(Calendar.MONTH);
16         final int day = calendar.get(Calendar.DAY_OF_MONTH);
17         int hour = calendar.get(Calendar.HOUR_OF_DAY);
18         int minute = calendar.get(Calendar.MINUTE);
19 
20         ... ...
21 
22         return new AlertDialog.Builder(getActivity())
23                 .setView(v)
24                 .setTitle(R.string.time_picker_title)
25                 // .setPositiveButton(android.R.string.ok, null)
26                 .setPositiveButton(android.R.string.ok,
27                         new DialogInterface.OnClickListener() {
28                             @Override
29                             public void onClick(DialogInterface dialog, int which) {
30                                 int hour, minute;
31 
32                                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
33                                     hour = mTimePicker.getHour();
34                                     minute = mTimePicker.getMinute();
35                                 } else {
36                                     hour = mTimePicker.getCurrentHour();
37                                     minute = mTimePicker.getCurrentMinute();
38                                 }
39                                 Date date = new GregorianCalendar(
40                                         year, month, day, hour, minute).getTime();
41                                 sendResult(Activity.RESULT_OK, date);
42                             }
43                         })
44                 .create();
45     }
46 
47     private void sendResult(int relustCode, Date date) {
48         if (getTargetFragment() == null) {
49             return;
50         }
51 
52         Intent intent = new Intent();
53         intent.putExtra(EXTRA_DATE, date);
54 
55         getTargetFragment().onActivityResult(getTargetRequestCode(), relustCode, intent);
56     }
57 }

 

这里,calendar在面谈过crime日期和时间是一个“整体”,由crime日期创建的,在TimePickerFragment保持日期值不变,仅仅运许用户调整时间。

 

再切换到CrimeFragment中,覆盖onActivityResult(...)方法,从extra中获取日期数据,增加“请求代码”的判断,依据“请求代码”:

  • 对DatePicker值,设置对应Crime的记录日期,然后刷新日期按钮的显示;
  • 对TimePIcker值,设置对应Crime的记录时间,然后刷新时间按钮的显示。

 

另外,同日期显示一样,为避免代码冗余,可以将时间按钮文字显示代码,封装到updateTiime()公共方法中,然后分别调用。

 

相关代码:

 1 public class CrimeFragment extends Fragment {
 2 
 3     ... ...
 4 
 5     @Override
 6     public View onCreateView(
 7             LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
 8 
 9         ... ...
10 
11         mTimeButton = (Button) v.findViewById(R.id.crime_time);
12         updateTime();
13         ... ...
14 
15     }
16 
17     @Override
18     public void onActivityResult(int requestCode, int resultCode, Intent data) {
19         if (resultCode != Activity.RESULT_OK) {
20             return;
21         }
22 
23         Date date = (Date) data
24                 .getSerializableExtra(DatePickerFragment.EXTRA_DATE);
25         mCrime.setDate(date);
26 
27         switch (requestCode) {
28             case REQUEST_DATE:
29                 updateDate();
30                 break;
31             case REQUEST_TIME:
32                 updateTime();
33                 break;
34         }
35     }
36 
37     private void updateDate() {
38         mDateButton.setText(DateFormat.format("EEEE, MMMM d, yyyyy", mCrime.getDate()));
39     }
40 
41     private void updateTime() {
42         mTimeButton.setText(DateFormat.format("h:mm a", mCrime.getDate()));
43     }
44 }

 

到此,12.4的跳转练习就完成了。完整的代码在GitHub上可以找到。

 

请高手指点这样添加是否存在什么隐患?谢谢!

 

《Android权威编程指南(The Big Nerd Ranch Guide)(第二版)》12.4挑战练习