在移动App里,有几种常见的界面形式:
手机上:一个列表界面A,点击某个条目后进入详情界面B,左右滑动可以切换到上/下条的详情界面;
平板上:由于屏幕足够大,列表界面A和详情界面B可以同时显示在屏幕上,分列两侧;
顶/底部若干标签,点击或者左右滑动可以显示不同的界面。
在Android上的解决方案之一是ViewPager + FragmentPagerAdapter + Fragment
,在iOS上的解决方案之一是UICollectionView + UICollectionViewCell
.
《安卓权威编程指南 Android Programming - The Big Nerd Ranch Guide》这本书的第7~12章、16~22章完成了一个功能复杂的APP开发,对于学习M(mode)V(view)C(controller)模式开发非常有益处,并且涉及非常多的主题:
如何创建并添加一个Fragment到Activity;
xml:样式style、主题theme、dp/sp、布局参数layout parameter、边距和内边距margin,padding;
使用ListFragment显示列表(介绍了如何使用单例构建数据模型,如何抽象类、ListView是如何从ArrayAdapter获取数据并呈现视图);
如何创建ArrayAdapter来管理ListView的数据;
如何响应ListView条目的点击事件;
如何定制化ListView条目的布局(默认的布局仅是一个textview);
如何从Fragment中启动并把参数传递给另一个Activity;
如何从Activity传递参数给它托管的Fragment(直接获取Activity extra,通过fragment argument bundle);
如何通知Fragment的Hosting Activity返回结果;
如何在Fragment内获取返回结果;
如何通过ViewPager托管Fragment,以实现屏幕滑动的效果;
如何在同一个Activity托管的两个Fragment之间传递数据;
如何根据屏幕大小选择布局;
贰跟着这些章节顺序完整地实现了这个App,对于理解上述主题,非常有帮助。并且通过GitHub记录了实现的过程:
下面gif展示的内容涉及到Fragment和ViewPager:这篇笔记主要是整理总结与Fragment相关的部分,整理自上述提到的章节。
Fragment是什么 & Activity如何管理一个Fragment
UI Fragment可以管理界面,整屏或部分。有自己的布局文件,包含了用户可以交互的可视化UI元素。
用UI Fragment将应用的UI分解成块,利用一个个构建块,很容易做到构建分页界面、动画侧边栏界面等更多其他定制界面。Fragment不具有在屏幕上显示视图的能力。因此,只有将它的视图放置在activity的视图层级结构中(称之为托管Hosting UI Fragment),fragment视图才能显示在屏幕上。在activity代码中添加fragment, 可以在运行时控制fragment,我们可以决定何时将fragment添加到activity中以及随后可以完成何种具体任务;也可以移除fragment,用其他fragment代替当前fragment,然后再重新添加已移除的fragment。
需要在activity视图层级结构中为fragment视图安排位置,创建fragment容器布局:
step1/3 创建Fragment容器布局
activity_crime.xml
step2/3 创建UI Fragment
但此时,activity还未托管fragment,所以代码运行后UI看不到任何内容。接下来需要编写代码,创建UI Fragment,覆写fragment的生命周期函数(几乎对应到activity的声明周期函数)。
创建fragment和创建activity步骤相同:定义布局文件、创建fragment子类、在代码中关联布局文件声明的组件。
public class CrimeFragment extends Fragment { private Crime mCrime; private EditText mTitleField; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mCrime = new Crime(); } @Override // 由onCreateView方法生成fragment的视图 public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { View v = inflater.inflate(R.layout.fragment_crime, parent, false); // 调用View.findViewById(int) mTitleField = (EditText)v.findViewById(R.id.crime_title); // 监听器方法设置和activity一样 mTitleField.addTextChangedListener(new TextWathcer() { public void onTextChanged(...) {} public void beforeTextChanged(...) {} public void afterTextChanged(...) {} }); return v; }}
step3/3 添加UI Fragment到FragmentManager
但此时运行,仍然看不到fragment,还需要将fragment的视图放置到FrameLayout容器中,以添加给activity。所以并没有“start fragment”这个概念。
FragmentManager类负责管理fragment并将它们的视图添加到activity的视图层级结构中。fragment transactions(事务)被用来添加、移除、附加、分离或替换fragment队列中的fragment。这是使用fragment在运行时组装和重新组装用户界面的核心方式。FragmentManager管理着fragment transactions的回退栈。
import android.support.v4.app.Fragmentpublic class CrimeActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_crime); FragmentManager fm = getSupportFragmentManager(); // 使用R.id.fragmentContainer的容器视图资源ID,向FragmentManager请求获取fragment。如要获取的fragment在队列中已经存在,FragmentManager随即会将之返还。 Fragment fragment = fm.findFragmentById(R.id.fragmentContainer); // 如指定容器视图资源ID的fragment不存在,则fragment变量为空值。 // 这时应创建一个新的CrimeFragment,并创建一个新的fragment transaction用来把新建的fragment添加到队列中。 if (fragment == null) { fragment = new CrimeFragment(); fm.beginTransaction() .add(R.id.fragmentContainer, fragment) .commit(); } }}
FragmentManager保持fragment与activity的状态一致,但fragment方法究竟是在activity方法之前还是之后调用的这一点是无法保证的。
使用ListFragment显示列表
ListView只有在需要显示某些列表项(list item是listview的一个child view object,可以是简单地 view,可以是复杂的view)时,它才会去申请可用的视图对象;如果为所有的列表项数据创建视图对象,会浪费内存;
ListView找谁去申请视图对象呢? 答案是adapter。adapter是一个控制器对象,负责从模型层获取数据,创建并填充必要的视图对象,将准备好的视图对象返回给ListView;
首先,通过调用adapter的getCount()方法,ListView询问数组列表中包含多少个对象(为避免出现数组越界的错误);紧接着ListView就调用adapter的getView(int, View, ViewGroup)方法。
创建默认的列表项
public class CrimeListFragment extends ListFragment{ private static final String TAG = "CrimeListFragment"; private ArrayListmCrimes; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getActivity().setTitle(R.string.crimes_title); mCrimes = CrimeLab.get(getActivity()).getCrimes(); // android.R.layout.simple_list_item_1)是Android SDK提供的列表项的布局资源,仅包含一个TextView; // 默认的ArrayAdapter .getView(...)方法依赖于toString()方法。 // toString()方法等价于getClass().getName() + '@' + Integer.toHexString(hashCode()) ,返回了混和对象类名和内存地址的字符串信息。 ArrayAdapter adapter = new ArrayAdapter (getActivity(), android.R.layout.simple_list_item_1, mCrimes); setListAdapter(adapter); } @Override public void onListItemClick(ListView l, View v, int position, long id) { Crime c = (Crime) l.getAdapter().getItem(position); Log.d(TAG, c.getTitle() + " was clicked"); }}
定制列表项
创建定义列表项视图的XML布局文件,替代默认的列表项布局文件;
创建ArrayAdapter的子类,并override
public View getView(int position, View convertView, ViewGroup parent)
Get a View that displays the data at the specified position in the data set. 而默认的getView方法调用的是toString方法;
getView执行时,首先检查是否有复用对象converView,没有则从定制的布局文件中inflate一个新的视图;然后调用Adapter.getItem()方法获取position位置的对象;再然后引用视图对象中的组件,赋予对象的信息;
然后在CrimeListFragment中绑定定制的adapter,更新onCreate(...)和onListItemClick(...)
由于CheckBox默认是focusable的,点击列表项时会被解读为切换checkbox状态,然后就无法触发onListItemClick(),所以出现在列表项布局内的任何可聚焦组件(如CheckBox或Button)都应设置为非聚焦状态,从而保证用户在点击列表项后能够获得预期效果。
public class CrimeListFragment extends ListFragment{ - ArrayAdapteradapter =- new ArrayAdapter (getActivity(), android.R.layout.simple_list_item_1, mCrimes);+ CrimeAdapter adapter = new CrimeAdapter(mCrimes); + private class CrimeAdapter extends ArrayAdapter {+ public CrimeAdapter(ArrayList crimes) {+ super(getActivity(), 0, crimes);+ }++ @Override+ public View getView(int position, View convertView, ViewGroup parent) {+ // If we weren't given a view, inflate one+ if (convertView == null) {+ convertView = getActivity().getLayoutInflater().inflate(R.layout.list_item_crime, null);+ }+ + // Configure the view for this crime+ Crime c = getItem(position);+ TextView titleTextView = (TextView) convertView.findViewById(R.id.crime_list_item_titleTextView);+ ……+ return convertView;+ }+ } }
重新加载显示列表项
如模型层保存的数据发生改变(或可能发生改变),应通知列表视图的adapter,以便其及时获取最新数据并重新加载显示列表项。在适当的时点,与系统的ActivityManager回退栈协同运作,可以完成列表项的刷新。
CrimeListActivity恢复运行状态后,操作系统会向它发出调用onResume()生命周期方法的指令。CrimeListActivity接到指令后,它的FragmentManager会调用当前被activity托管的fragment的onResume()方法。一般来说,要保证fragment视图得到刷新,在onResume()方法内更新代码是最安全的选择。(因为可能只是暂停,而不是停止onStart方法不会被调用到)
public class CrimeListFragment extends ListFragment{+ @Override+ public void onResume() {+ super.onResume();+ ((CrimeAdapter)getListAdapter()).notifyDataSetChanged();+ }
Fragment和Activity之间数据传递
典型的应用场景:ActivityA及其托管的FragmentA,ActivityB及其托管的FragmentB。现在需要从FragmentA中启动并传递数据给ActivityB,ActivityB再把数据传递给FragmentB。
从Fragment启动Activity
从fragment中启动activity的实现方式,基本等同于从activity中启动另一activity的实现方式。调用Fragment.startActivity(Intent)方法,该方法在后台会调用对应的Activity方法;
附加extra信息;
fragment如何从托管它的activity获取extra信息?简单的方法是getActivity().getIntent(),但牺牲了fragment的封装性,因为它总是需要由某个具体activity托管着。
weiyiWorkCell:Learning-Android-CriminalIntent weiyi$ git diff a31d00d 9d3ebdb public class CrimeListFragment extends ListFragment{ @Override public void onListItemClick(ListView l, View v, int position, long id) { Crime c = ((CrimeAdapter) l.getAdapter()).getItem(position);- Log.d(TAG, c.getTitle() + " was clicked");+ // Start Activity+ Intent i = new Intent(getActivity(), CrimeActivity.class);+ i.putExtra(CrimeFragment.EXTRA_CRIME_ID, c.getId());+ startActivity(i); }diff --git a/src/me/li2/android/criminalintent/CrimeFragment.java b/src/me/li2/android/criminalintent/CrimeFragment.java public class CrimeFragment extends Fragment {+ public static final String EXTRA_CRIME_ID = "me.li2.android.criminalintent.crime_id"; // Configure the fragment instance. public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);- mCrime = new Crime();+ UUID crimeId = (UUID) getActivity().getIntent().getSerializableExtra(EXTRA_CRIME_ID);+ mCrime = CrimeLab.get(getActivity()).getCrime(crimeId); }
附加arguments给Fragment,获取arguments
每个fragment实例都可附带一个Bundle对象。该bundle包含有key-value对,我们可以如同附加extra到Activity的intent中那样使用它们。一个key-value对即一个argument.
首先需创建Bundle对象;然后使用Bundle限定类型的“put”方法(类似于Intent的方法),将argument添加到bundle中。
附加argument bundle给fragment,必须在fragment创建后、添加给activity前完成。所以习惯做法是添加名为newInstance()的静态方法给Fragment类。
使用该方法,完成fragment实例及bundle对象的创建,然后将argument放入bundle中,最后再附加给fragment。托管activity需要fragment实例时,需调用newInstance()方法,而非直接调用其构造方法。
托管activity就应该知道有关托管fragment方法的细节,但fragment则不必知道其托管activity的细节问题。至少在需要保持fragment通用独立性的时候是如此。
weiyiWorkCell:Learning-Android-CriminalIntent weiyi$ git diff 9d3ebdb acc17afdiff --git a/src/me/li2/android/criminalintent/CrimeActivity.java b/src/me/li2/android/criminalintent/CrimeActivity.java public class CrimeActivity extends SingleFragmentActivity { @Override protected Fragment createFragment() {- return new CrimeFragment();+ UUID crimeId = (UUID) getIntent().getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID);+ return new CrimeFragment().newInstance(crimeId); } } public class CrimeFragment extends Fragment {+ public static CrimeFragment newInstance(UUID crimeId) {+ Bundle args = new Bundle();+ args.putSerializable(EXTRA_CRIME_ID, crimeId);+ + CrimeFragment fragment = new CrimeFragment();+ fragment.setArguments(args);+ return fragment;+ }+ @Override // Configure the fragment instance. public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);- UUID crimeId = (UUID) getActivity().getIntent().getSerializableExtra(EXTRA_CRIME_ID);+ UUID crimeId = (UUID) getArguments().getSerializable(EXTRA_CRIME_ID); }
在Fragment内获取返回结果
fragment有自己的
Fragment.startActivityForResult(Intent,int)
和onActivityResult(...)
,但却不具有setResult(...)
方法,但可以先取得托管它的activity,然后再返回数据。
weiyiWorkCell:Learning-Android-CriminalIntent weiyi$ git diff 8d87138 155c5d8diff --git a/src/me/li2/android/criminalintent/CrimeFragment.java b/src/me/li2/android/criminalintent/CrimeFragment.java public class CrimeFragment extends Fragment {+ public void returnResult() {+ getActivity().setResult(Activity.RESULT_OK, null);+ } }diff --git a/src/me/li2/android/criminalintent/CrimeListFragment.java b/src/me/li2/android/criminalintent/CrimeListFragment.java public class CrimeListFragment extends ListFragment{+ private static final int REQUEST_CRIME = 1;- startActivity(i);+ startActivityForResult(i, REQUEST_CRIME);+ @Override+ public void onActivityResult(int requestCode, int resultCode, Intent data) {+ if (requestCode == REQUEST_CRIME) {+ // Handle result+ } }
ViewPager和Fragment
创建CrimePagerActivity类来管理ViewPager;
定义包含ViewPager的视图层级结构:以代码的方式构建ViewPager:创建资源id,创建实例,设置实例的id,设置为activity的内容视图content view。
在CrimePagerActivity类中关联使用ViewPager及其PagerAdapter(包括FragmentStatePagerAdapter和FragmentPagerAdapter),二者间的配合支持实际归结为两个简单方法的使用,即getCount()和getItem(int)。调用getItem(int)方法获取crime数组指定位置的Crime时,它会返回一个已配置的用于显示指定位置crime信息的CrimeFragment。
修改CrimeListFragment.onListItemClick(...)方法,启动CrimePagerActivity,而非CrimeActivity.
ViewPager.setCurrentItem()方法设置当前显示的page;
使用OnPageChangeListener监听ViewPager当前显示页面的状态变化。onPageScrolled(...)方法可告知我们页面将会滑向哪里;onPageScrollStateChanged(...)方法可告知我们当前页面所处的行为状态,如正在被用户滑动、页面滑动入位到完全静止以及页面切换完成后的闲置状态。
public class CrimePagerActivity extends FragmentActivity { private ViewPager mViewPager; private ArrayListmCrimes; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mViewPager = new ViewPager(this); mViewPager.setId(R.id.viewPager); setContentView(mViewPager); mCrimes = CrimeLab.get(this).getCrimes(); FragmentManager fm = getSupportFragmentManager(); mViewPager.setAdapter(new FragmentStatePagerAdapter(fm) { @Override public int getCount() { return mCrimes.size(); } @Override // 因为需要返回Fragment(用于构建activity),所以在构建adapter时,还需传入FragmentManager给它的构造方法。 public Fragment getItem(int pos) { Crime crime = mCrimes.get(pos); return CrimeFragment.newInstance(crime.getId()); } }); mViewPager.setOnPageChangeListener(new OnPageChangeListener() { @Override public void onPageSelected(int pos) { Crime crime = mCrimes.get(pos); if (crime.getTitle() != null) { setTitle(crime.getTitle()); } } @Override public void onPageScrolled(int pos, float posOffset, int posOffsetPixels) { } @Override public void onPageScrollStateChanged(int state) {} }); UUID crimeId = (UUID) getIntent().getSerializableExtra(CrimeFragment.EXTRA_CRIME_ID); for (int i=0; i
创建并显示日期选择的对话框(把AlertDialog视图封装在DialogFragment实例中)
不使用DialogFragment,也可显示AlertDialog视图,但Android开发原则不推荐这种做法。使用FragmentManager管理对话框,可使用更多配置选项来显示对话框,比如:gravity, match_parent.
要将DialogFragment添加给FragmentManager管理并放置到屏幕上,可调用DialogFragment.show(FragmentManager fm, String tag)方法。
res/layout/dialog_date.xml
src/me/li2/android/criminalintent/DatePickerFragment.java
public class DatePickerFragment extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { // 虽然DatePicker可以直接生成视图,但使用布局文件容易修改对话框的显示内容 // DatePicker v = new DatePicker(getActivity()); View v = getActivity().getLayoutInflater().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(); }}
src/me/li2/android/criminalintent/CrimeFragment.java
public static CrimeFragment newInstance(UUID crimeId) { mDateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FragmentManager fm = getActivity().getSupportFragmentManager(); DatePickerFragment dialog = new DatePickerFragment(); dialog.show(fm, DIALOG_DATE); } });
同一个Activity托管的两个Fragment如何发送数据
到现在为止,CrimeActivity已经包含了2个Fragment:CrimeFragment(继承自Fragment)和DatePickerFragment(继承自DialogFragment)。
问题是,当用户点击CrimeFragment的日期按钮时,怎么把日期传递给DatePickerFragment?类似从activity传递数据到fragment,替代fragment的构造方法,创建和设置fragment argument通常是在一个newInstance()方法中完成。
weiyiWorkCell:Learning-Android-CriminalIntent weiyi$ git diff 62a68ee 0c6fe64diff --git a/src/me/li2/android/criminalintent/CrimeFragment.java b/src/me/li2/android/criminalintent/CrimeFragment.java public class CrimeFragment extends Fragment { @Override public void onClick(View v) { FragmentManager fm = getActivity().getSupportFragmentManager();- DatePickerFragment dialog = new DatePickerFragment();+ DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate()); dialog.show(fm, DIALOG_DATE); } });diff --git a/src/me/li2/android/criminalintent/DatePickerFragment.java b/src/me/li2/android/criminalintent/DatePickerFragment.java public class DatePickerFragment extends DialogFragment {+ public static final String EXTRA_DATE = "me.li2.android.criminalintent.date";+ + private Date mDate;+ + public static DatePickerFragment newInstance(Date date) {+ Bundle args = new Bundle();+ args.putSerializable(EXTRA_DATE, date);+ + DatePickerFragment fragment = new DatePickerFragment();+ fragment.setArguments(args);+ + return fragment;+ }+ @Override public Dialog onCreateDialog(Bundle savedInstanceState) {+ mDate = (Date) getArguments().getSerializable(EXTRA_DATE); …… // 虽然DatePicker可以直接生成视图,但使用布局文件容易修改对话框的显示内容 // DatePicker v = new DatePicker(getActivity()); View v = getActivity().getLayoutInflater().inflate(R.layout.dialog_date, null); return new AlertDialog.Builder(getActivity()) .setView(v) .setTitle(R.string.date_picker_title)
同一个Activity托管的两个Fragment如何回传结果
现在用户选择完了日期,点击确定按钮后,如何把这个日期数据从DatePickerFragment回传到CrimeFragment呢?
Fragment提供来了另外一种绑定方式:为fragment设置数据返回的目标fragment和请求码:调用Fragment.setTargetFragment(Fragment fragment, int requestCode)。
然后通过getTargetFragment().onActivityResult(getTargetRequestCode(), int resultCode, Intent data)方法实现数据的回传。
weiyiWorkCell:Learning-Android-CriminalIntent weiyi$ git diff 0c6fe64 5a572dcdiff --git a/src/me/li2/android/criminalintent/CrimeFragment.java b/src/me/li2/android/criminalintent/CrimeFragment.java public class CrimeFragment extends Fragment {+ private static final int REQUEST_DATE = 0; mDateButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FragmentManager fm = getActivity().getSupportFragmentManager(); DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());+ dialog.setTargetFragment(CrimeFragment.this, REQUEST_DATE); dialog.show(fm, DIALOG_DATE); } });+ @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);+ }+ } }diff --git a/src/me/li2/android/criminalintent/DatePickerFragment.java b/src/me/li2/android/criminalintent/DatePickerFragment.java public class DatePickerFragment extends DialogFragment { + private void sendResult(int resultCode) {+ if (getTargetFragment() == null) {+ return;+ }+ + Intent i = new Intent();+ i.putExtra(EXTRA_DATE, mDate);+ getTargetFragment().onActivityResult(getTargetRequestCode(), resultCode, i);+ }+ @Override public Dialog onCreateDialog(Bundle savedInstanceState) { 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) {+ sendResult(Activity.RESULT_OK);+ }+ }) .create(); }+ }
整理自《安卓权威编程指南 Android Programming - The Big Nerd Ranch Guide》第7~12章、16~22章。
版权声明:《如何使用Android UI Fragment开发“列表-详情”界面?》由 WeiYi.Li 在 2015年09月05日写作。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
文章链接: