您的位置:首页 > 编程语言

跟Google学写代码:使用Fragment构建可变的界面

2016-06-24 16:08 417 查看

项目介绍

运行示例:



UML类图:



控制层:MainActivity

视图层:HeadlinesFragment 的listview, ArticleFragment的textview

数据层:Ipsum

代码分析

首先是MainActivity的布局news_articles.xml,考虑适配,我们写好两种layout满足不同尺寸的设备

普通模式:

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />


同时在layout-large目录下也有news_articles.xml,通过xml静态定义MainActivity的布局,针对不同尺寸的手机设备,会加载不同的布局

平板模式(large layout)

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent">

<fragment android:name="com.example.android.fragments.HeadlinesFragment"
android:id="@+id/headlines_fragment"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="match_parent" />

<fragment android:name="com.example.android.fragments.ArticleFragment"
android:id="@+id/article_fragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="match_parent" />

</LinearLayout>


MainActivity.java类

public class MainActivity extends FragmentActivity
implements HeadlinesFragment.OnHeadlineSelectedListener {

/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.news_articles);
//检查activity使用的布局版本是否为普通模式下的fragment_container FrameLayout
//如果是,我们需要动态添加fragment
//如果不是,activity会通过直接加载large-layout模式下的xml文件,来静态加载两个fragment

if (findViewById(R.id.fragment_container) != null) {
//这里使用findViewById的方式值得一学

// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null) {
return;
}

// 创建一个Fragment实例,它其实是ListFragment的子类实现
HeadlinesFragment firstFragment = new HeadlinesFragment();

// In case this activity was started with special instructions from an Intent,
// pass the Intent's extras to the fragment as arguments
firstFragment.setArguments(getIntent().getExtras());
//将Fragment添加到'fragment_container' 布局中(记住下面四部曲)
//get->begi-add->com
getSupportFragmentManager().beginTransaction()
.add(R.id.fragment_container, firstFragment).commit();
}
}

/**
* 用户操作HeadlinesFragment时,回调这个接口
*/
public void onArticleSelected(int position) {

//在activity中创建article fragment
ArticleFragment articleFrag = (ArticleFragment)
getSupportFragmentManager().findFragmentById(R.id.article_fragment);

if (articleFrag != null) {
//如果article fragment是可用的,意味着目前处于large-layout模式下,activity静态加载了两个fragment,

//回调ArticleFragment的update方法 通知页面更新
articleFrag.updateArticleView(position);

} else {

//如果fragment不可用,意味着我们需要去动态添加到布局中,并且替换当前的HeadLineFragment,让ArticleFragment显示到屏幕上

//创建article fragment,并且通知article fragment 选中了哪一个标题,以此来作为根据显示哪一个内容
ArticleFragment newFragment = new ArticleFragment();
Bundle args = new Bundle();

//这里使用ArticleFragment中定义好的.ARG_POSITION的静态字符串常量作为标记
//而不是在Activity中定义静态字符串常量,这种小细节值得注意
args.putInt(ArticleFragment.ARG_POSITION, position);
newFragment.setArguments(args);

FragmentTransaction transaction = getSupportFrag`这里写代码片`mentManager().beginTransaction();

// 用上一步创建好的article fragment来替换当前fragment_container正在显示的HeadLineFragment
//addToBackStack可以让用户向后导航,即退出ArticleFragment时,也可以看到HeadLinFragment的neritic
//如果不使用addToBackStack,点击返回键 会退出当前activity
transaction.replace(R.id.fragment_container, newFragment);
//设置切换动画
int setTransition=TRANSIT_FRAGMENT_OPEN;
transaction.setTransition(setTransition);
transaction.addToBackStack(null);

// 最后记得使用事物提交 fragment的操作
transaction.commit();
}
}
}


总结一下MainActivity中的 小亮点:

实现HeadlinesFragment的回调接口OnHeadlineSelectedListener,将业务代码放在Activity中,这是一个简单的解耦,下文会解释如何解耦

做了屏幕适配:考虑小屏的情况,动态添加布局;当设备为大屏时,加载资源文件large-layout中的布局,静态初始化两个fragment

通过事物的setTransition来设置切换动画

考虑用户回退操作,即为用户做了向后导航的功能,通过事物的addToBackStack来完成

接着是首先要展示的界面HeadlinesFragment,它继承自ListFragment,ListFragment是一种特殊的fragment,可以直接通过setAdapter来为它加载布局,同时可以使用android提供的默认list布局:

public class HeadlinesFragment extends ListFragment {

//内部自定义的回调接口
OnHeadlineSelectedListener mCallback;

// 只要Activity实现此接口,就可以通过HeadLineFragment通知ArticleFragment来更新内容了
public interface OnHeadlineSelectedListener {
/** 这个接口的方法,在本类的onListItemClikc()中被调用,这样就可以让Activity来回调了 */
public void onArticleSelected(int position);
}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

//在使用Android提供的默认list布局时,需要考虑兼容不同的版本
// We need to use a different list item layout for devices older than Honeycomb
int layout = Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB ?
android.R.layout.simple_list_item_activated_1 : android.R.layout.simple_list_item_1;

//适配器和来自Ipsum的HeadLines数组绑定起来,
setListAdapter(new ArrayAdapter<String>(getActivity(), layout, Ipsum.Headlines));
}

@Override
public void onStart() {
super.onStart();

//当处于large-layout模式时,为选中的item设置为高亮模式
//当然前提是article  fragment是可见的
if (getFragmentManager().findFragmentById(R.id.article_fragment) != null) {
getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
//setChoiceMode 这个API值得一记!
}
}

@Override
public void onAttach(Activity activity) {
super.onAttach(activity);

//下面两句代码,确保程序员在Activity中实现了OnHeadlineSelectedListener接口,如果没有实现,抛出异常
//这种写法强调了一种代码规范,即必须按照接口实现规定的内容

try {
mCallback = (OnHeadlineSelectedListener) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnHeadlineSelectedListener");
}
}

@Override
public void onListItemClick(ListView l, View v, int position, long id) {
// 通知activity选中的位置
mCallback.onArticleSelected(position);

//在larg-layout模式下,通过setItemChecked将选中的item设置高亮状态,这样就不用我们自己实现状态选择器了
getListView().setItemChecked(position, true);
}
}


总结一下HeadLineFragment中的 亮点:

内部定义了一个回调接口OnHeadlineSelectedListener,交给Activity去实现,业务逻辑放在Activity中,而不是直接在HeadLineFragment的onListItemClick中,这样既完成了解耦:Head fragment和 Article Fragment之间相互不依赖,不持有彼此的引用(实例对象),也因为将业务逻辑放在activity中而使得代码逻辑更清晰,这其实就是MVP的体现。

listview可以通过setChoiceMode() 设置高亮模式

在onAttach中使用抛出异常的作法,强调了一种 代码规范,每个程序员在使用HeadLineFragment时,必须在Activity中实现此接口。 代码规范的作用,就是告诉每一个程序员,你应该做什么,摒弃个人编码习惯,顺应团队的编码规范,才是王道

listview可以通过setItemChecked设置高亮模式

ArticleFragment.java

public class ArticleFragment extends Fragment {
final static String ARG_POSITION = "position";
int mCurrentPosition = -1;

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {

// If activity recreated (such as from screen rotate), restore
// the previous article selection set by onSaveInstanceState().
// This is primarily necessary when in the two-pane layout.
//考虑Fragment 运行时可能发生改变,可以从Bundle对象中恢复数据,用一个变量来保存
//这种写法在large-layout模式下非常有用

if (savedInstanceState != null) {
mCurrentPosition = savedInstanceState.getInt(ARG_POSITION);
}

return inflater.inflate(R.layout.article_view, container, false);
}

@Override
public void onStart() {
super.onStart();

//在启动fragment过程中,检查是否将参数传递给当前fragment
//onStart是一个绝佳的位置来更新视图,比如设置textview的内容
//为什么在onStart设置视图?因为在onCreateView中已经加载完毕布局

Bundle args = getArguments();
if (args != null) {
//基于外部传进来的索引,来设置article fragment的内容
updateArticleView(args.getInt(ARG_POSITION));
} else if (mCurrentPosition != -1) {
//基于onCreateView初始化的mCurrentPosition来设置 内容
updateArticleView(mCurrentPosition);
}
}

public void updateArticleView(int position) {
TextView article = (TextView) getActivity().findViewById(R.id.article);
article.setText(Ipsum.Articles[position]);
mCurrentPosition = position;
}

@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
//保存选中的位置,以备fragment可能重建的情况
outState.putInt(ARG_POSITION, mCurrentPosition);
}
}


ArticleFragment 中有 几个小亮点值得注意:

mCurrentPosition 临时变量来保存 上一次ArticleFragment显示的内容(其实是索引,根据索引调用Inpus中的字符串数组Articles来显示内容),防止因为屏幕旋转,或者用户其他行为,导致需要重建页面时 数据(索引)丢失的情况,onSaveInstate的用法大家都不陌生,只是什么时候用,需要提前考虑好,而不是bug出现了,才去加代码解决,好好提升代码质量把!

在onStart中设置视图内容,这是一种非常安全的作法,以免在onCreatView中设置视图内容时,出现空指针异常

ArticleFragment 的布局article_view.xml很简单,就一个textview:

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/article"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:textSize="18sp" />


Ipsum.java

public class Ipsum {

static String[] Headlines = {
"Article One",
"Article Two"
};

static String[] Articles={"Article One \n\n 本人的老爸有个朋友,以前身体不舒服去检查,结果医生告诉他," +
"病得有点严重,这个要控制,那个也要控制,要不然会很快恶化。\n" +
"忍了一段时间后他受不了了,由于经济条件不错,所以他有什么想吃的就吃,有什么想喝的就喝,过得相当自在。\n" +
"结果就这样过了很多很多年,可能是心态影响身体,当初给他看病的那几个医生基本都挂了,他还活得活蹦乱跳的……",
//Article Two----------------------
"Article Two \n\n  端午佳节博得你一笑 祝你节日快乐!\n" + "老外来到中国吃粽子,吃完后深感体会的说!你们中国的粽子真好吃,就是包粽子的那几棵青菜太老了,好半天才咽下去![呲牙][呲牙][呲牙]"
};

}


总结

1. 代码规范:MainAcitity实现接口的方式,来对两个Fragment解耦

2. 屏幕适配:创建默认layout和大屏模式large-layout两种情况的布局

3. 切换动画:setTrasition()设置Fragment 切换动画

4. listview设置高亮:setChoiceMode(),setItemChecked()

5. Fragment状态发生改变时,数据的保存和恢复:onSaveInstanceState(),临时变量记录数据,Bundle存储

6. 多个Fragment切换,通过addToBackStack()实现用户向后导航的功能

7. 在onStart()中为Fragment的视图设置数据

资源

Building a Dynamic UI with Fragments

示例demo1

示例demo2
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  代码规范 fragment