《Android权威编程指南(The Big Nerd Ranch Guide)(第二版)》12.4挑战练习
本书第12章是讲解Dialog。12.4挑战练习是在CriminalIntent项目中,再增加一个TimePickerFragment的对话框fragment。通过在CrimeFragment用户界面上添加的时间按钮,
弹出TimePickerFragment界面,允许用户使用TimePicker组件选择crime发生的具体时间。
我的修改思路是:
- 按照DatePickerFragment实现的步骤、方法实现实现TimePickerFragment;
- crime日期与时间是一个整体:
- DatePickerFragment仅可以调整:年月日,时分不变动;
- TimePickerFragment仅可以调整:时分,时分不变动;
- 故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上可以找到。
请高手指点这样添加是否存在什么隐患?谢谢!