目录
- 【一】 前言
- 【二】 App介绍
- 【三】 App开发
- 零、 开发环境
- 一、 初始化准备
- 二、 创建Model类`Crime`
- 三、 配置好`String.xml`资源文件
- 四、 创建一个单`Fragment`的抽象`SingleFragmentActivity`
- 五、 创建一个`CrimeFragment`
- 六、 创建一个单例类`CrimeLab`
- 七、 利用同样的方法创建一个`CrimeListActivity`(extends `SingleFragemntActivity`)
- 八、理清`RecyclerView` 、 `ViewHolder` 和 `Adapter`的关系
- 九、 将`RecyclerView`部署到`CrimeListFragment`中
- 十、更新item为`constraint_layout`
- 十一、添加跳转逻辑,实现list中item内容的更新
- 十二、 使用`ViewPager`
- 十三、增加对话框
【一】 前言
按照我《Android编程权威指南(第三版)》读书笔记这篇博客,可以知道我们入门的这篇教材中一共有8个App,除去最开始的像“Hello World”一样的入门App【GeoQuiz】以外,这个【CriminalIntent】1项目也算是第一个比较完整的项目,作为我们Android项目的入门示例也很合适。那么我们就开始脚踏实地的一步步开始着手开发这个App吧!
【二】 App介绍
这是一个主要由两个Activity
组成的App,所有的界面逻辑都依托于Fragment
。该App是用来记录办公室(或者是日常生活中)的各种同事、同伴、同学的陋习。并且能够标记为解决。
本项目仍然还是使用MVC架构
【三】 App开发
零、 开发环境
Android Studio 3.2.1
Build #AI-181.5540.7.32.5056338, built on October 9, 2018
JRE: 1.8.0_152-release-1136-b06 amd64
JVM: OpenJDK 64-Bit Server VM by JetBrains s.r.o
Windows 10 10.0
一、 初始化准备
- 首先在Android Studio新建一个只有
Empty Activity
的项目,主Activity的名称改为CrimeActivity
。使用下面的命令初始化一个git仓库。
$ git init
第一步就不多做解释了。
- 添加
.gitignore
文件(如果已经自动创建了,稍微检查一下有没有需要ignore的文件被漏掉):
# .gitignore文件
# Built application files
*.apk
*.ap_
# Files for the Dalvik VM
*.dex
# Java class files
*.class
# Generated files
bin/
gen/
# Local configuration file (sdk path, etc)
local.properties
# OSX files
.DS_Store
# Android Studio
*.iml
.idea
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
随后进行第一次git commit
$ git add *
$ git commit -m "初始化CriminalIntent项目"
- 为了使用Fragment,要在
app/build.gradle
(不是项目根目录中的build.gradle,而是app文件夹内的) 文件中的dependencies
中确保有:
dependencies {
...
implementation 'com.android.support:support-v4:28.0.0'
...
}
这样一句依赖。
二、 创建Model类Crime
- 认识
UUID
类型:
类的全名为:java.Utils.UUID
,直接继承自Serializable
类,是一个final class
。
首先理解一下UUID
的含义Universally Unique Identifier,即通用唯一识别码。2
该类能够保证生成的序列是唯一的,适合用来作为数据库的Primary Key。
使用也很简单:
UUID.randomUUID();
- 声明Crime类的成员变量,并且写明构造函数。
public class Crime {
private UUID mId;
private String mTitle;
private Date mDate;
private boolean mSolved;
public Crime(){
mId = UUID.randomUUID();
mDate = new Date();
}
}
而后为所有变量生成getter方法,为除了mId
以外的所有变量生成setter方法。
三、 配置好String.xml
资源文件
首先将String.xml
文件中需要配置的字符串资源配置好:
<resources>
<string name="app_name">CriminalIntent</string>
<string name="crime_title_hint">Enter a title for the crime.</string>
<string name="crime_title_label">Title</string>
<string name="crime_details_label">Details</string>
<string name="crime_solved_label">Solved</string>
</resources>
这四个字符串信息就如名字一样,后续可能会添加其他多语言的翻译。
四、 创建一个单Fragment
的抽象SingleFragmentActivity
在Android Studio中直接以以下方式创建:
先把这个java文件放在一边,我们先去创建一个xml
文件,回来再处理它。
方便起见我们直接修改创建项目时候直接生成的布局文件activity_crime.xml
的文件名为activity_single_fragment.xml
并且在其中只放一个FrameLayout
作为单个fragment
的容器:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CrimeActivity">
</FrameLayout>
好的现在我们有了这个布局文件再回过头来看看前面的抽象类。
要使用FragmentManager
在其中添加fragment
,但是添加前还要先判断其中是否有fragment
:
public abstract class SingleFragmentActivity extends AppCompatActivity {
protected abstract Fragment createFragment();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single_fragment);
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_container);
if(fragment == null){
fragment = createFragment();
fragmentManager.beginTransaction()
.add(R.id.fragment_container, fragment)
.commit();
}
}
}
五、 创建一个CrimeFragment
这一步骤开始,我们先来回顾一下Fragment
的生命周期。比Activity
稍微复杂一些:
只是多了一个有关view和一个有关activity的生命周期部分。
所以在实现时候要注意重写生命周期回调函数。
然后将布局文件修改如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".CrimeFragment">
<TextView
style="?android:listSeparatorTextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_title_label"/>
<EditText
android:id="@+id/crime_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/crime_title_hint"/>
<TextView
style="?android:listSeparatorTextViewStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_details_label"/>
<Button
android:id="@+id/crime_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<CheckBox
android:id="@+id/crime_solved"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/crime_solved_label"/>
</LinearLayout>
<注意>:如果你使用了convert view的功能把默认的FrameLayout
转化成了LinearLayout
,那么切记一定要添加一句
android:orientation="vertical"
否则这些东西都挤在一排就看不见啦!
切换到 设计(design) 视图预览一下:
然后修改CrimeFragment.java
文件,进行以下操作:
- 初始化内部成员
mCrime
- 绑定视图
- 绑定控件并设置监听
最终代码如下:
public class CrimeFragment extends Fragment {
// 一个CrimeFragment绑定一个crime
private Crime mCrime;
// fragment中的ui控件
private EditText mEtTitle;
private Button mBtDate;
private CheckBox mCbSolved;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mCrime = new Crime();
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_crime, container, false);
// 绑定UI控件
mEtTitle = view.findViewById(R.id.et_crime_title);
mBtDate = view.findViewById(R.id.bt_crime_date);
mCbSolved = view.findViewById(R.id.cb_crime_solved);
// 设置监听器
mEtTitle.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// 文字改变前
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// 文字改变时,更新Crime的title
mCrime.setTitle(s.toString());
}
@Override
public void afterTextChanged(Editable s) {
// 文字改变后
}
});
mCbSolved.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mCrime.setSolved(isChecked);
}
});
mBtDate.setText(mCrime.getDate().toString());
mBtDate.setClickable(false);
return view;
}
}
好的,基本已经完成了。那么最后一步,要将Fragment
绑定到Activity
上。因为Fragment
本身不具备展现到屏幕上的能力。
所幸前面已经做好了拓展化的准备,现在只要将CrimeActivity
的超类设置为SingleFragmentActivity
并进行少许修改即可:
public class CrimeActivity extends SingleFragmentActivity {
@Override
protected Fragment createFragment() {
return new CrimeFragment();
}
}
现在运行app就能看到一个简单的界面了。
六、 创建一个单例类CrimeLab
暂时没有多线程访问,所以只要简单使用单例设计模式3即可:
public class CrimeLab {
private static CrimeLab sCrimeLab;
public static CrimeLab get(Context context) {
if (sCrimeLab == null) {
sCrimeLab = new CrimeLab(context);
}
return sCrimeLab;
}
private CrimeLab(Context context) {
}
}
注意这里的构造函数和get()
方法都使用了Context
作为参数,是为了读取其中的信息,暂时用不到,后面会使用的。
后续不要忘记在CrimeLab
中以List
的形式存放好Crime
。
也别忘记添加获取Crime
的方法。
七、 利用同样的方法创建一个CrimeListActivity
(extends SingleFragemntActivity
)
创建一个无布局文件的CrimeListActivity
和一个有布局文件的CrimeListFragment
:
首先CrimeListActivity
很简单,继承自SingleFragmentActivity
而实现的方法只需要实现createFragment()
方法为返回一个new
出来的CrimeListFragment
即可。
最后在AndroidManifest.xml
文件中把这个新的Activity设置为启动Activity即可:
...
<activity android:name=".CrimeActivity">
</activity>
<activity android:name=".CrimeListActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
...
而Fragment部分暂时留空,现在应该就能正常运行app了~(虽然打开应该是一片空白= =!)
八、理清RecyclerView
、 ViewHolder
和 Adapter
的关系
这三者是合作实现动态管理滑动的view的,而且三者的功能解耦度很高:
RecyclerView
的任务仅限于回收和定位屏幕上的ViewViewHolder
顾名思义是占位,容纳View用的。这套方法不会直接创建视图,创建的是ViewHolder , 而ViewHolder引用着 itemViewAdapter
创建ViewHolder,是一个控制器对象,从模型层获取数据,然后提供给 RecyclerView 显示,是沟通的桥梁。它负责创建必要的ViewHolder和绑定ViewHolder至模型层数据。
九、 将RecyclerView
部署到CrimeListFragment
中
首先添加依赖,在app/build.gradle
中添加以下内容:
dependnencies{
...
implementation 'com.android.support:recyclerview-v7:28.0.0'
}
然后直接把fragment_crime_list.xml
整个布局文件的根view改变为RecyclerView
:
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/rv_crime_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CrimeListFragment">
</android.support.v7.widget.RecyclerView>
随后为了后续添加list的item,新建一个布局文件list_item_crime.xml
:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/tv_crime_title"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/tv_crime_date"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
现在这个item还很简单,后续会慢慢丰富。
随后在CrimeListFragment.java
中创建一个ViewHolder
的子类和一个Adapter
的子类:
public class CrimeListFragment extends Fragment {
...
private class CrimeHolder extends RecyclerView.ViewHolder{
// item中的UI控件
private TextView mTvCrimeTitle, mTvCrimeDate;
public CrimeHolder(LayoutInflater inflater, ViewGroup parent) {
super(inflater.inflate(R.layout.list_item_crime, parent, false));
mTvCrimeTitle = itemView.findViewById(R.id.tv_crime_date);
mTvCrimeDate = itemView.findViewById(R.id.tv_crime_date);
}
public void bind(Crime crime){
mTvCrimeTitle.setText(crime.getTitle());
mTvCrimeDate.setText(crime.getDate().toString());
}
}
private class CrimeAdapter extends RecyclerView.Adapter<CrimeHolder>{
private List<Crime> mCrimes;
public CrimeAdapter(List<Crime> crimes){
mCrimes = crimes;
}
@NonNull
@Override
public CrimeHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
LayoutInflater inflater = LayoutInflater.from(getActivity());
return new CrimeHolder(inflater, viewGroup);
}
@Override
public void onBindViewHolder(@NonNull CrimeHolder crimeHolder, int i) {
crimeHolder.bind(mCrimes.get(i));
}
@Override
public int getItemCount() {
return mCrimes.size();
}
}
}
写完这些以后,不要忘了及时对RecyclerView
进行setAdapter(...)
,否则Adapter
是无法正确显示内容到屏幕上的~
做好上述操作之后,可以再对ViewHolder设置一下点击的回调函数,简单弹出toast就可以,方便后续操作。
总之这时候运行程序就能看到下面的样子啦:
十、更新item为constraint_layout
首先先来回顾一下三种view尺寸设置:
那么首先可以把item_list_crime.xml
文件的根view转化为constraint_layout
,然后再向项目中的./src/res/
文件夹内复制图片资源,方便起见可以下载我的资源,解压后复制到该文件夹内:https://download.youkuaiyun.com/download/sakura_white/10826751
下面添加ImageView
,并且通过拖拽的方式添加constraint,最后的布局文件变为下面的样子:(list_item_crime.xml):
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/linearLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:layout_editor_absoluteY="81dp">
<TextView
android:id="@+id/tv_crime_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:textColor="@android:color/black"
android:textSize="18sp"
app:layout_constraintEnd_toStartOf="@+id/iv_solved"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_crime_date"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/iv_solved"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_crime_title" />
<ImageView
android:id="@+id/iv_solved"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_solved" />
</android.support.constraint.ConstraintLayout>
别忘了在CrimeListFragment.java
文件中的CrimeHolder
内部类里设置一下图片的setVisible()
属性,最后运行效果如下:

对了,日期的setText()
部分可以使用下面的方法规范一下日期格式:
DateFormat.format("yyyy年MM月dd日 kk:mm:ss, E", mCrime.getDate())
十一、添加跳转逻辑,实现list中item内容的更新
虽然现在App已经有模有样了,但是点击List中的任何一个Crime还是只能弹出一个Toast
消息,实在是不够称得上一个合格的APP,为了方便后续的跳转,我们到CrimeFragment.java
中去添加一个创建实例的方法newInstance()
:
public class CrimeFragment extends Fragment {
...
private static final String ARG_CRIME_ID = "crime_id";
public static Fragment newInstance(UUID crimeId){
Bundle args = new Bundle();
args.putSerializable(ARG_CRIME_ID, crimeId);
Fragment crimeFragment = new CrimeFragment();
crimeFragment.setArguments(args);
return crimeFragment;
}
}
有个连锁效应。之前我们新建的CrimeFragment都是凭空new出来的,所以也就没有初始的信息。现在都是通过点击item跳转出来的,所以现在要在list的onbind中初始化视图:
public class CrimeFragment extends Fragment {
public View onCreateView(@NonNull LayoutInflater inflater
, @Nullable ViewGroup container
, @Nullable Bundle savedInstanceState) {
...
// 更新UI
mEtTitle.setText(mCrime.getTitle());
mBtDate.setText(DateFormat.format("yyyy年MM月dd日 kk:mm:ss, E", mCrime.getDate()));
mCbSolved.setChecked(mCrime.isSolved());
}
}
好的那么下面继续回到正题,跳转逻辑上来。
我们都知道fragment是必须依托在activity中的,刚刚虽然创建了CrimeFragment.newInstance()
方法,但是说实话并不能够实现真正的跳转,毕竟Activity间通讯还是要靠Intent
老大哥,所以在CrimeActivity.java
中,我们添加这样一个newIntent(UUID crimeId)
方法:(顺便重写的createFragnment()
方法也要稍加改动)
public class CrimeActivity extends SingleFragmentActivity {
private static final String EXTRA_CRIME_ID = "com.issane.criminalintent.crime_id";
public static Intent newIntent(Context context, UUID crimeId){
Intent intent = new Intent(context, CrimeActivity.class);
intent.putExtra(EXTRA_CRIME_ID, crimeId);
return intent;
}
@Override
protected Fragment createFragment() {
UUID crimeId = (UUID) getIntent().getSerializableExtra(EXTRA_CRIME_ID);
return CrimeFragment.newInstance(crimeId);
}
}
有了这样一个newIntent()
和一个newInstance()
函数,只要之后在跳转的时候调用CrimeActivity.newIntent(Context context, UUID crimeId)
函数就可以实现跳转到特定的Crime了。
不过最后别忘了,要对CrimeListFragment中做一下更新操作,否则按返回键之后看到的List还会是一开始的。4
public class CrimeListFragment extends Fragment {
...
@Override
public void onResume() {
super.onResume();
updateUI();
}
private void updateUI(){
CrimeLab crimeLab = CrimeLab.get(getActivity());
List<Crime> crimes = crimeLab.getCrimes();
if(mAdapter == null){
mAdapter = new CrimeAdapter(crimes);
mRvCrimeList.setAdapter(mAdapter);
} else {
mAdapter.notifyDataSetChanged();
}
}
}
十二、 使用ViewPager
之前的话只能靠点击一个Crime然后按返回键回到list,再在list里去找下一个Crime点,有的时候体验就会很不顺畅,所以要使用ViewPager
,实现滑动切换Crime,提升体验。
这一步骤按书上的说明,大概要完成三个任务:
- 创建一个
CrimePagerActivity.java
- 构建布局,定义包含ViewPager的视图层级结构
- 在
CrimePagerActivity.java
中关联使用Adapter和ViewPager
其实ViewPager的配置倒是不复杂,只要把Adapter绑定正确就好,下面是CrimePagerActivity的onCreate()
:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_crime_pager);
UUID crimeId = (UUID) getIntent().getSerializableExtra(EXTRA_CRIME_ID);
mCrimes = CrimeLab.get(this).getCrimes();
mVpCrimePager = findViewById(R.id.vp_crime_pager);
FragmentManager fragmentManager = getSupportFragmentManager();
mVpCrimePager.setAdapter(new FragmentStatePagerAdapter(fragmentManager) {
@Override
public Fragment getItem(int i) {
return CrimeFragment.newInstance(mCrimes.get(i).getId());
}
@Override
public int getCount() {
return mCrimes.size();
}
});
// 设置初始Crime位置
for (int i = 0; i < mCrimes.size(); i++){
if(mCrimes.get(i).getId().equals(crimeId)){
mVpCrimePager.setCurrentItem(i, true);
break;
}
}
}
虽然这部分很简单,但是本书本章后面专门介绍了一下ViewPager的工作原理:
ViewPager和PagerAdapter在后台为我们完成了很多工作。本节我们来深入学习ViewPager的工作原理。
继续之前,先提个醒:大多数情况下,我们无需了解其内部实现细节。不过,如果要自己实现PagerAdapter接口,则需了解ViewPager-PagerAdapter和Recycler-View-Adapter各自关系的异同。
什么时候需要自己实现PagerAdapter接口呢?
需要ViewPager托管非fragment视图时(如图片这样的常见View对象),就需要实现原生PagerAdapter接口。
为什么选择使用ViewPager而不是RecyclerView呢?
由于无法使用现有的Fragment,因此在CriminalIntent应用中使用RecyclerView需处理大量内部实现工作。Adapter需要我们及时地提供View。然而,决定fragment视图何时创建的是FragmentManager。因此,当RecyclerView要求Adapter提供fragment视图时,我们无法立即创建fragment并提供其视图。
这就是ViewPager存在的理由。它使用的是PagerAdapter类,而非原来的Adapter。
PagerAdapter要比Adapter复杂得多,因为它要处理更多的视图管理工作。以下为它的内部
实现。PagerAdapter不使用可返回视图的onBindViewHolder(…)方法,而是使用下列方法:
public Object instantiateItem(ViewGroup container, int position)
public void destroyItem(ViewGroup container, int position, Object object)
public abstract boolean isViewFromObject(View view, Object object)
PagerAdapter.instantiateItem(ViewGroup, int)方法告诉pager adapter创建指定位置的列表项视图,然后将其添加给ViewGroup视图容器,而destroyItem(ViewGroup, int,Object)方法则告诉pager adapter销毁已建视图。注意,instantiateItem(ViewGroup, int)方法并不要求立即创建视图。因此,PagerAdapter可自行决定何时创建视图。
视图创建完成后,ViewPager会在某个时间点看到它。为确定该视图所属的对象,ViewPager会调用isViewFromObject(View, Object) 方法。这里, Object 参数是instantiateItem(ViewGroup,int)方法返回的对象。因此,假设ViewPager调用instantiateItem(ViewGroup, 5)方法返回A对象,那么只要传入的View参数是第5个对象的视图,isViewFromObject(View, A)方法就应返回true值,否则返回false值。对于ViewPager来说,这是一个复杂的过程,但对于PagerAdapter来说,这算不上什么,
因为PagerAdapter只要能够创建、销毁视图以及识别视图来自哪个对象即可。这样的要求显然
很宽松,因而PagerAdapter能够比较自由地通过instantiateItem(ViewGroup, int)方法创
建并添加新的fragment,然后返回可以跟踪管理的Object(fragment)。以下为isViewFromObject
(View, Object)方法的具体实现:
@Override
public boolean isViewFromObject(View view, Object object) {
return ((Fragment)object).getView() == view;
}
可以看到,每次需要使用ViewPager时,都要覆盖实现PagerAdapter的这些方法,这真是
一种磨难。幸好,还有FragmentPagerAdapter和FragmentStatePagerAdapter。真心感谢它们!
十三、增加对话框
AlertDialog
类,即对话框。
Google提供了旧版的原生对话框,以及新的AppCompat的兼容库版的对话框(需要引入android.support.v7.app.AlertDialog
依赖项)
建议将AlertDialog封装在DialogFragment(Fragment的子类)实例中使用。当然,不使用DialogFragment也可显示AlertDialog视图,但不推荐这样做。使用FragmentManager管理对话框,可以更灵活地显示对话框。
另外,如果旋转设备,单独使用的AlertDialog会消失,而封装在fragment中的AlertDialog则不会有此问题(旋转后,对话框会被重建恢复)。
所以,我们的项目自然就会使用一个DialogFragment
的子类来进行操作。
在这个子类里面,我们需要重写onCreateDialog(Bundle bundle)
方法。将该方法的返回值设置为一个AlertDialog
(使用了Builder模式),所以具体的创建AlertDialog
的方法如下:
new AlertDialog.Builder(getActivity())
.setTitle(R.string.date_picker_title)
.setPositiveButton(android.R.string.ok, null) // 这个null应该是监听器,暂时留空
.create();
然后在CrimeFragemnt
中我们之前不是添加了一个日期按钮并且设置不可点击了嘛,现在把它的onClick()回调函数修改成显示刚刚创建的DialogFragment
子类视图xxxxxxFragment
:
FragmentManager manager = getFragmentManager();
DatePickerFragment dialog = new xxxxxxFragment();
dialog.show(manager, DIALOG_DATE);
如果不出意外,那么现在该按钮应该已经能够正常显示对话框了,只是现在显示的十一个空空的对话框,后面我们还是想要添加一个选择日期的工具DatePicker组件,为了实现这一步骤,还是最好写一个布局文件dialog_date.xml
出来,这个布局文件的根view就是一个DatePicker,并且不要忘了在利用Builder新建AlertDialog的时候添加一句setView(),这一函数的参数自然就是要用inflater来inflate出一个view啦~
View v = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_date, null); // 注意这里的root参数为null
当然了,只是点击一个日历也没什么意思,这里最后我们最终目标还是说得能够实现数据的传递。
为返回新日期给CrimeFragment,并更新模型层以及对应视图,需将日期打包为extra并附加到Intent上,然后调用CrimeFragment.onActivityResult(…)方法,并传入准备好的Intent参数,如下图所示。
在稍后的实现代码中可以看到,我们没有调用托管activity的Activity.onActivityResult(…)方法,而是调用了Fragment.onActivityResult(…)方法,这似乎令人费解。实际上,调用onActivityResult(…)方法实现fragment间的数据传递不仅行得通,而且可以更灵活地展现对话框fragment(稍后会看到)。
为了实现封装并且做好argument的添加,最好就是实现一个newInstance()
方法,并且用写好的静态常量字符串作为TAG标记好。
但是Date并不能直接配置Calendar类,所以要进行下面的操作:
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);
mDatePicker = (DatePicker) v.findViewById(R.id.xxxxxxxx); // xxxxxx是你在布局文件中定义的ID
mDatePicker.init(year, month, day, null);
项目代码随时更新在我的coding.net上:https://coding.net/u/CERN/p/CriminalIntent ↩︎
todo:研究UUID唯一性的原理 ↩︎
todo:更新设计模式博客,写明单例模式的学习 ↩︎
todo:这里后面是可以考虑不更新整个Adapter只更新其中一个Item的。 ↩︎