【0基础Android】从零开始的Android小app——CriminalIntent

【一】 前言

按照我《Android编程权威指南(第三版)》读书笔记这篇博客,可以知道我们入门的这篇教材中一共有8个App,除去最开始的像“Hello World”一样的入门App【GeoQuiz】以外,这个【CriminalIntent1项目也算是第一个比较完整的项目,作为我们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

一、 初始化准备

  1. 首先在Android Studio新建一个只有Empty Activity的项目,主Activity的名称改为CrimeActivity。使用下面的命令初始化一个git仓库。
$ git init

第一步就不多做解释了。

  1. 添加.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项目"
  1. 为了使用Fragment,要在app/build.gradle (不是项目根目录中的build.gradle,而是app文件夹内的) 文件中的dependencies中确保有:
dependencies {
    ...
    implementation 'com.android.support:support-v4:28.0.0'
    ...
}

这样一句依赖。

二、 创建Model类Crime

  1. 认识UUID类型:
    类的全名为:java.Utils.UUID,直接继承自Serializable类,是一个final class
    首先理解一下UUID的含义Universally Unique Identifier,即通用唯一识别码2
    该类能够保证生成的序列是唯一的,适合用来作为数据库的Primary Key。
    使用也很简单:
UUID.randomUUID();
  1. 声明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中直接以以下方式创建:

SingleFragmentActivity

先把这个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稍微复杂一些:
Fragment的生命周期

只是多了一个有关view和一个有关activity的生命周期部分。

所以在实现时候要注意重写生命周期回调函数。
创建一个fragment

然后将布局文件修改如下:

<?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预览

然后修改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了~(虽然打开应该是一片空白= =!)

八、理清RecyclerViewViewHolderAdapter的关系

这三者是合作实现动态管理滑动的view的,而且三者的功能解耦度很高:

  • RecyclerView的任务仅限于回收和定位屏幕上的View
  • ViewHolder顾名思义是占位,容纳View用的。这套方法不会直接创建视图,创建的是ViewHolder , 而ViewHolder引用着 itemView
  • Adapter创建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就可以,方便后续操作。

总之这时候运行程序就能看到下面的样子啦:
添加RecyclerView后的app

十、更新item为constraint_layout

首先先来回顾一下三种view尺寸设置:
三种view尺寸设置

那么首先可以把item_list_crime.xml文件的根view转化为constraint_layout,然后再向项目中的./src/res/文件夹内复制图片资源,方便起见可以下载我的资源,解压后复制到该文件夹内:https://download.csdn.net/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()属性,最后运行效果如下:

改为constraint_layout后的list

对了,日期的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,提升体验。

利用ViewPager实现滑动翻页

这一步骤按书上的说明,大概要完成三个任务:

  • 创建一个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参数,如下图所示。

CrimeFragment和DatePickerFragment之间的事件流

在稍后的实现代码中可以看到,我们没有调用托管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);

  1. 项目代码随时更新在我的coding.net上:https://coding.net/u/CERN/p/CriminalIntent ↩︎

  2. todo:研究UUID唯一性的原理 ↩︎

  3. todo:更新设计模式博客,写明单例模式的学习 ↩︎

  4. todo:这里后面是可以考虑不更新整个Adapter只更新其中一个Item的。 ↩︎

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值