介绍:CriminalIntent应用能记录陋习的标题,日期以及照片,也支持在联系人当中查找当事人,通过E-mail,Twitter,FaceBook或其他应用提出抗议。本博客是在上一个博客的基础上进行的延伸。
- 第12章:日期对话框,fragment数据传递;
- 第13章:工具栏,菜单,层级式导航;
- 第14章:SQLite数据库的使用;
项目完成步骤:
- 添加对话框
- 创建工具栏菜单
- 连接数据库
对话框
为应用添加对话框,以便用户修改crime记录日期。用户点击
CrimeFragment中的日期按钮时,会弹出对话框:
创建DialogFragment
首先会创建名为DatePickerFragment的DialogFragment子类。然后,在DatePickerFragment中,创建并配置显示DatePicker组件的AlertDialog实例。DatePickerFragment同样由CrimePagerActivity托管。图12-2展示了以上各对象间的关系。
图12-2 由CrimePagerActivity托管的两个fragment对象要显示对话框,首先应完成以下任务:
创建DatePickerFragment类;
创建AlertDialog;
借助FragmentManager在屏幕上显示对话框。
稍后,我们还将配置使用DatePicker,并实现在CrimeFragment和DatePickerFragment之间传递数据。
创建DialogFragment
public class DatePickerFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle saveInstanceState){
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.date_picker_title)
.setPositiveButton(android.R.string.ok, null)
.create();
}
}
显示DialogFragment
public class CrimeFragment extends Fragment {
private static final String ARG_CRIME_ID = "crime_id";
private static final String DIALOG_DATE = "DialogDate";
...
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
...
mDateButton = (Button) v.findViewById(R.id.crime_date);
mDateButton.setText(mCrime.getDate().toString());
//mDateButton.setEnabled(false);
mDateButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
FragmentManager manager = getFragmentManager();
DatePickerFragment dialog = new DatePickerFragment();
dialog.show(manager, DIALOG_DATE);
}
});
mSolvedCheckBox = (CheckBox) v.findViewById(R.id.crime_solved);
...
return v;
}
...
}
运行CriminalIntent应用,点击日期按钮弹出对话框:
图12-3 带标题和OK按钮的AlertDialog给AlertDialog添加DatePicker
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
View v = LayoutInflater.from(getActivity())
.inflate(R.layout.dialog_date, null);
return new AlertDialog.Builder(getActivity())
.setView(v)
.setTitle(R.string.date_picker_title)
.setPositiveButton(android.R.string.ok, null)
.create();
}
运行CriminalIntent应用,点击日期按钮,在对话框中显示DatePicker视图:
图12-4 DatePicker视图
fragment间的数据传递
图12-5CrimeFragment与DatePickerFragment间的对话传递数据给DatePickerFragment
public class DatePickerFragment extends DialogFragment {
public static final String EXTRA_DATE =
"com.bignerdranch.android.criminalintent.date";
private static final String ARG_DATE = "date";
private DatePicker mDatePicker;
public static DatePickerFragment newInstance(Date date){
Bundle args = new Bundle();
args.putSerializable(ARG_DATE,date);
DatePickerFragment fragment = new DatePickerFragment();
fragment.setArguments(args);
return fragment;
}
@Override
public Dialog onCreateDialog(Bundle saveInstanceState){
Date date = (Date) getArguments().getSerializable(ARG_DATE);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int day = calendar.get(Calendar.DAY_OF_MONTH);
View v = LayoutInflater.from(getActivity())
.inflate(R.layout.dialog_date,null);
mDatePicker = (DatePicker) v.findViewById(R.id.dialog_date_picker);
mDatePicker.init(year,month,day,null);
return new AlertDialog.Builder(getActivity())
.setView(v)
.setTitle(R.string.date_picker_title)
//.setPositiveButton(android.R.string.ok,null)
.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).getTime();
sendResult(Activity.RESULT_OK,date);
}
})
.create();
}
private void sendResult(int resultCode,Date date){
if(getTargetFragment()==null){
return;
}
Intent intent = new Intent();
intent.putExtra(EXTRA_DATE,date);
getTargetFragment()
.onActivityResult(getTargetRequestCode(),resultCode,intent);
}
}
public class CrimeFragment extends Fragment {
public static CrimeFragment newInstance(UUID crimeId) {
Bundle args = new Bundle();
args.putSerializable(ARG_CRIME_ID, crimeId);
CrimeFragment fragment = new CrimeFragment();
fragment.setArguments(args);
return fragment;
}
...
@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();
}
}
...
private void updateDate() {
mDateButton.setText(mCrime.getDate().toString());
}
}
工具栏
为CriminalIntent应用创建工具栏菜单,提供新增crime记录的菜单项,并实现向上按钮的导航功能:
图13-1 CriminalIntent应用的工具栏使用AppCompat库
添加AppCompat依赖项;
使用一种AppCompat主题;
确保所有的activitiy都是AppCompatActivity子类。
使用AppCompat主题
<resources>
<style name="AppTheme"parent="Theme.AppCompat.Light.DarkActionBar">
</style>
</resources>
工具栏菜单
菜单及菜单项需用到一些字符串资源。将它们添加到strings.xml文件中。
<resources>
...
<string name="date_picker_title">Date of crime:</string>
<string name="new_crime">New Crime</string>
<string name="show_subtitle">Show Subtitle</string>
<string name="hide_subtitle">Hide Subtitle</string>
<string name="subtitle_format">%1$s crimes</string>
</resources>
创建菜单资源
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_item_new_crime"
android:icon="@android:drawable/ic_menu_add"
android:title="@string/new_crime"
app:showAsAction="ifRoom|withText"/>
</menu>
创建菜单
在代码中,Activity类提供了管理菜单的回调函数。需要选项菜单时,Android会调用Activity的onCreateOptionsMenu(Menu)方法。然而,按照CriminalIntent应用的设计,选项菜单相关的回调函数需在fragment而非activity里实现。不用担心,Fragment有一套自己的选项菜单回调函数。稍后,我们会在CrimeListFragment中实现这些方法。以下为创建菜单和响应菜单项选择事件的两个回调方法:
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater)
public boolean onOptionsItemSelected(MenuItem item)
在CrimeListFragment.java中,覆盖onCreateOptionsMenu(Menu, MenuInflater)方法,实例化fragment_crime_list.xml中定义的菜单。
实例化选项菜单
@Override
public void onResume() {
super.onResume();
updateUI();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.fragment_crime_list, menu);
}
在以上方法中,调用MenuInflater.inflate(int, Menu)方法并传入菜单文件的资源ID,将布局文件中定义的菜单项目填充到Menu实例中。
响应菜单项选择事件
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
super.onCreateOptionsMenu(menu,inflater);
inflater.inflate(R.menu.fragment_crime_list,menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
switch (item.getItemId()){
case R.id.new_crime:
Crime crime = new Crime();
CrimeLab.get(getActivity()).addCrime(crime);
Intent intent = CrimePagerActivity
.newIntent(getActivity(),crime.getId());
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
实现层级式导航
...
<activity
android:name=".CrimePagerActivity"
android:label="@string/app_name"
android:parentActivityName=".CrimeListActivity">
</activity>
...
图13-2 CrimePagerActivity界面上的向上按钮
SQLite数据库
创建数据库前,首先要清楚存储什么样的数据。CriminalIntent应用要保存的是一条条crime记录,定义Schema的方式众多,如何选择往往因人而异。处理类似的任务,开发人员都有个共同的目标:“不要重复造轮子。”实际上,这也是人人都应遵守的编程准则:多花时间思考复用代码的编写和调用,避免在应用中到处使用重复代码。
基于上述准则,我们可以使用能统一定义模型层对象(如Crime)的高级ORM(对象关系映射)工具。不过,对于CriminalIntent应用,本章打算直接在Java代码中定义描述表名和数据字段的数据库schema。
首先,我们来创建定义schema的Java类。创建时,命名类为CrimeDbSchema,同时在新建类对话框中输入包名database.CrimeDbSchema。这样,就可以将CrimeDbSchema.java文件放入专门的database包中,实现数据库操作相关代码的组织和归类。
定义CrimeTable内部类
public class CrimeDbSchema {
public static final class CrimeTable {
public static final String NAME = "crimes";
}
}
定义数据表字段
public class CrimeDbSchema {
public static final class CrimeTable {
public static final String NAME = "crimes";
public static final class Cols {
public static final String UUID = "uuid";
public static final String TITLE = "title";
public static final String DATE = "date";
public static final String SOLVED = "solved";
}
}
}
创建初始数据库
步骤:
(1) 确认目标数据库是否存在。
(2) 如果不存在,首先创建数据库,然后创建数据库表以及必需的初始化数据。
(3) 如果存在,打开并确认CrimeDbSchema是否是最新版本(后续章节可能会在CriminalIntent
中有增删操作)。
(4) 如果是旧版本,就运行相关代码升级到最新版本。
创建CrimeBaseHelper类编写SQL创建初始代码
public class CrimeBaseHelper extends SQLiteOpenHelper {
private static final int VERSION = 1;
private static final String DATABASE_NAME = "crimeBase.db";
public CrimeBaseHelper(Context context){
super(context,DATABASE_NAME,null, VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("create table " + CrimeDBSchema.CrimeTable.NAME);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
打开SQLiteDatabase
public class CrimeLab {
private static CrimeLab sCrimeLab;
private List<Crime> mCrimes;
private Context mContext;
private SQLiteDatabase mDatabase;
...
private CrimeLab(Context context) {
mContext = context.getApplicationContext();
mDatabase = new CrimeBaseHelper(mContext)
.getWritableDatabase();
mCrimes = new ArrayList<>();
}
...
}
调用getWritableDatabase()方法时,CrimeBaseHelper要做如下工作。
(1)
打开/data/data/com.bignerdranch.android.criminalintent/databases/crimeBase.db数据库;如果不存在,就先创建crimeBase.db数据库文件。
(2) 如果是首次创建数据库,就调用onCreate(SQLiteDatabase)方法,然后保存最新的版本号。
(3) 如果已创建过数据库,首先检查它的版本号。如果CrimeOpenHelper中的版本号更高,就调用onUpgrade(SQLiteDatabase, int, int)方法升级。
最后,再做个总结:onCreate(SQLiteDatabase)方法负责创建初始数据库;onUpgrade (SQLiteDatabase, int, int)方法负责与升级相关的工作。
修改 CrimeLab 类
创建完数据库,接下来是调整CrimeLab类代码,改用mDatabase存储数据。
首先要删除CrimeLab中所有mCrimes相关的代码,代码调整完毕,运行CriminalIntent应用只会看到空列表和空CrimePagerActivity。
写入数据库
- 使用 ContentValues
- 插入和更新记录
读取数据库
- 使用 CursorWrapper
- 创建模型层对象
总结
编写代码过程中难免遇到单词拼写错误的情况,只要细心检查,错误都是可以解决的。但是遇到手机与电脑连接不起来、虚拟机开启失败等问题,就很难自己解决了。虚拟机时而运行成功时而运行失败,手机是直接安装不了APP,自己也找了很久,还是不能解决问题,也问了老师,但还是没能解决,不知道是哪里出了错。整个项目梳理的还算可以,就是运行不了让人头疼。
网友评论