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

项目介绍


运行示例:

这里写图片描述

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中的 小亮点:

  1. 实现HeadlinesFragment的回调接口OnHeadlineSelectedListener,将业务代码放在Activity中,这是一个简单的解耦,下文会解释如何解耦
  2. 做了屏幕适配:考虑小屏的情况,动态添加布局;当设备为大屏时,加载资源文件large-layout中的布局,静态初始化两个fragment
  3. 通过事物的setTransition来设置切换动画
  4. 考虑用户回退操作,即为用户做了向后导航的功能,通过事物的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中的 亮点:

  1. 内部定义了一个回调接口OnHeadlineSelectedListener,交给Activity去实现,业务逻辑放在Activity中,而不是直接在HeadLineFragment的onListItemClick中,这样既完成了解耦:Head fragment和 Article Fragment之间相互不依赖,不持有彼此的引用(实例对象),也因为将业务逻辑放在activity中而使得代码逻辑更清晰,这其实就是MVP的体现。
  2. listview可以通过setChoiceMode() 设置高亮模式
  3. 在onAttach中使用抛出异常的作法,强调了一种 代码规范,每个程序员在使用HeadLineFragment时,必须在Activity中实现此接口。 代码规范的作用,就是告诉每一个程序员,你应该做什么,摒弃个人编码习惯,顺应团队的编码规范,才是王道
  4. 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 中有 几个小亮点值得注意:

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

  2. 在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的视图设置数据

资源


  1. Building a Dynamic UI with Fragments
  2. 示例demo1
  3. 示例demo2
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值