Google示例APP,教你如何写出适配多种屏幕的新闻阅读器

前言

今天上午看了Google的示例APP,NewsReader,主要的意图是叫你如何在不同屏幕设备上加载不同的布局,还考虑了横向和纵向时的布局变化。让我这种只能在一种界面上写代码的人收益不少呢。除了这个重点,官方的代码也让我学到不少,简单,低耦合,无多余布局,充分利用系统特性。

我先列一下我新学到的东西,一会记录的时候好有个重点

  • 定义Fragment与Activity通信的两种方式
  • 如何在运行时确定当前设备应该加载的布局,且不同屏幕加载相同内容时无需重复定义
  • 如何根据加载的布局执行相应的操作
  • Tab Listener的灵活使用
  • 功能层层递进,关系分明,没有累述的类定义

一兴奋写的有点多,因为这个项目比较大,先来一个大概的影响吧。我在平板模拟器和手机模拟器上都运行了一下,效果如下图所示:

这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述

ps:最后一张是平板横向的显示,因为模拟器显示问题才会留出黑边。

下面开始代码分析。


正文

首先看一下项目的结构目录,有个概要,因为全说篇幅会很长,我就围绕这几重点展开。

这里写图片描述

先来看一下类:

新闻内容类:

NonsenseGenerator类是用来产生新闻内容的类,新闻的标题和内容都由它产生。

NewsArticle类 将NonsenseGenerator的内容加上< html >等拼装成网页语言(因为新闻直接用webview显示)。

NewsCategory类 每一个NewsCategory管理20篇NewsArticle,意思是这个类型的新闻有20篇。

NewsSource类 每一个NewsSource管理4个目录,至此为止新闻内容从底至上全部封装完成。

界面显示的类:

HeadlinesFragment 是显示新闻标题的Fragemnt,在大屏幕设备上和小屏设备都可使用,实现了Fragment复用。

ArticleFragment 是显示新闻内容的Fragment,它只在大屏设备的双面板使用

ArticleActivity 是小屏幕设备显示新闻内容的独立Activity,但内容直接使用ArticleFragment,也实现了复用。

导航Tab类

CompatActionBarNavHandler 是处理Tab的监听器类,但是只在API11及以上版本有效

CompatActionBarNavListener Tab监听器接口类,在主Activity继承实现,也是低耦合的一种体现。


定义Fragment与Activity通信的两种方式

第一种方式:

我在之前的博客就有介绍,因为Google告诉我们Fragment之间不应该存在直接通信,所以我们要完成Fragment之间的通信就要通过Fragment和Activity之间的桥梁完成。

大致步骤是,在Fragement定义一个接口,还有这个接口的成员变量,然后在Activity中实现这个接口,然后在Attach()方法里对这个接口的成员变量用Activity赋值,这样,Fragment如果想和Activity通信,只需要使用这个成员变量,然后调用接口的方法就可以了。

第二种方式:

和第一种方式基本一致,就是在赋值的时候没有在Fragment里赋值,而是在Activity里面调用Fragment的公有方法将Activity的上下文对象传递进来,然后对Fragment接口的成员变量进行赋值。

下面关于第二种方式的代码(我先把和这个内容无关的代码用省略号代替了):

HeadlinesFragment中的相关代码:

public class HeadlinesFragment extends ListFragment implements OnItemClickListener {

    ......

    // The listener we are to notify when a headline is selected
    OnHeadlineSelectedListener mHeadlineSelectedListener = null;

    /**
     * Represents a listener that will be notified of headline selections.
     */
    public interface OnHeadlineSelectedListener {
        /**
         * Called when a given headline is selected.
         * @param index the index of the selected headline.
         */
        public void onHeadlineSelected(int index);
    }

    /**
     * Default constructor required by framework.
     */
    public HeadlinesFragment() {
        super();
    }

    @Override
    public void onStart() {
        super.onStart();
        setListAdapter(mListAdapter);
        getListView().setOnItemClickListener(this);
        loadCategory(0);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mListAdapter = new ArrayAdapter<String>(getActivity(), R.layout.headline_item,
                mHeadlinesList);
    }

    /**
     * Sets the listener that should be notified of headline selection events.
     * @param listener the listener to notify.
     */
    public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {
        mHeadlineSelectedListener = listener;
    }



    /**
     * Handles a click on a headline.
     *
     * This causes the configured listener to be notified that a headline was selected.
     */
    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (null != mHeadlineSelectedListener) {
            mHeadlineSelectedListener.onHeadlineSelected(position);
        }
    }
    ......

NewsReaderActivity中的相关代码:

public class NewsReaderActivity extends FragmentActivity
        implements HeadlinesFragment.OnHeadlineSelectedListener,
                   CompatActionBarNavListener,
                   OnClickListener  {

    // Whether or not we are in dual-pane mode
    boolean mIsDualPane = false;

    // The fragment where the headlines are displayed
    HeadlinesFragment mHeadlinesFragment;

    // The fragment where the article is displayed (null if absent)
    ArticleFragment mArticleFragment;

    // The news category and article index currently being displayed
    int mCatIndex = 0;
    int mArtIndex = 0;
    NewsCategory mCurrentCat;

    // List of category titles
    final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

       ......

        mHeadlinesFragment.setOnHeadlineSelectedListener(this);

       ......
    }

    /** Called when a headline is selected.
     *
     * This is called by the HeadlinesFragment (via its listener interface) to notify us that a
     * headline was selected in the Action Bar. The way we react depends on whether we are in
     * single or dual-pane mode. In single-pane mode, we launch a new activity to display the
     * selected article; in dual-pane mode we simply display it on the article fragment.
     *
     * @param index the index of the selected headline.
     */
    @Override
    public void onHeadlineSelected(int index) {
        mArtIndex = index;
        if (mIsDualPane) {
            // display it on the article fragment
            mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
        }
        else {
            // use separate activity
            Intent i = new Intent(this, ArticleActivity.class);
            i.putExtra("catIndex", mCatIndex);
            i.putExtra("artIndex", index);
            startActivity(i);
        }
    }

    ......

我们看到NewsReaderActivity实现了接口,然后在OnCreate()方法里吧上下文传递给HeadlinesFragment,然后HeadlinesFragment把接口变量设置好。

这样做的好处是什么呢?Fragment只需要把它和Activity之间需要交互的方式定义好,而不是通过硬编码的方式去指定具体的任务,通过接口能很好的降低代码的耦合性,代码接口也显得更加清晰。


如何在运行时确定当前设备应该加载的布局,且不同屏幕加载相同内容时无需重复定义

首先我们看看一下res文件夹下面的资源目录:

这里写图片描述

要是搁以前我肯定不明白为什么会这样,values里面怎么都是布局文件。不仅如此,当我看到主Activity,也就是NewsReaderActivity在onCreate()方法里设备的布局文件我就惊了,它是下面这样的:

public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

哪来的main_layout,在逗我么?于是我打开values下面的layout文件,才明白。第一个layouts.xml里的内容是这样的:

<resources>
    <item name="main_layout" type="layout">@layout/onepane_with_bar</item>
    <bool name="has_two_panes">false</bool>
</resources>

于是昨天看了Google教程-针对多种屏幕进行设计的我瞬间就明白了。不懂可以看看这篇

因为有这么多的values文件,就是为多种不同屏幕设备而准备的,如果是手机,那它选择第一个layouts文件,它加载的布局就是onepane_with_bar这个布局文件,如果是最小宽度是600dp的平板电脑(横向时),那它选择的是sw600dp的layouts文件,然后加载的twopanes这个布局文件。

这是sw600dp-land里的layouts.xml文件内容:

<resources>
    <item name="main_layout" type="layout">@layout/twopanes</item>
    <bool name="has_two_panes">true</bool>
</resources>

我们看到,它是通过这种映射关系来告诉不同屏幕的设备应该要加载的布局。除了能针对各种设备给他们加载对应的布局外,这种映射的方式还有一个好处就是使用了别名。意思是说如果两个不同屏幕的设备所需要的布局是相同的,我们只需要把它们映射到相同别名的布局文件就行,这样避免了重复文件内容带来的维护问题。这里面的细节很多,如果大家感兴趣,可以参考这篇文章


如何根据加载的布局执行相应的操作

在手机上运行,大家可以看到开始只能显示新闻标题列表,而在平板上可以使用双面板模式,不仅使用标题列表还可以显示文章内容。这时因为设备加载了不同的布局,我们就不得不要采取相应的反应。比如说,用户点击标题列表,在手机上我们应该启动一个独立的Activity去显示对应的新闻内容,而在平板上我们就可以直接在右侧显示出新闻内容。

我贴一下主Activity里面的onCreate()方法,大家一看便知:

public class NewsReaderActivity extends FragmentActivity
        implements HeadlinesFragment.OnHeadlineSelectedListener,
                   CompatActionBarNavListener,
                   OnClickListener  {

    // Whether or not we are in dual-pane mode
    boolean mIsDualPane = false;

    // The fragment where the headlines are displayed
    HeadlinesFragment mHeadlinesFragment;

    // The fragment where the article is displayed (null if absent)
    ArticleFragment mArticleFragment;

    // The news category and article index currently being displayed
    int mCatIndex = 0;
    int mArtIndex = 0;
    NewsCategory mCurrentCat;

    // List of category titles
    final String CATEGORIES[] = { "Top Stories", "Politics", "Economy", "Technology" };

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_layout);

        // find our fragments
        mHeadlinesFragment = (HeadlinesFragment) getSupportFragmentManager().findFragmentById(
                R.id.headlines);
        mArticleFragment = (ArticleFragment) getSupportFragmentManager().findFragmentById(
                R.id.article);

        // Determine whether we are in single-pane or dual-pane mode by testing the visibility
        // of the article view.
        View articleView = findViewById(R.id.article);
        mIsDualPane = articleView != null && articleView.getVisibility() == View.VISIBLE;

        // Register ourselves as the listener for the headlines fragment events.
        mHeadlinesFragment.setOnHeadlineSelectedListener(this);

        // Set up the Action Bar (or not, if one is not available)
        int catIndex = savedInstanceState == null ? 0 : savedInstanceState.getInt("catIndex", 0);
        setUpActionBar(mIsDualPane, catIndex);

        // Set up headlines fragment
        mHeadlinesFragment.setSelectable(mIsDualPane);
        restoreSelection(savedInstanceState);

        // Set up the category button (shown if an Action Bar is not available)
        Button catButton = (Button) findViewById(R.id.categorybutton);
        if (catButton != null) {
            catButton.setOnClickListener(this);
        }
    }

其中,官方教程先找到两个Fragment,然后我们发现它是通过这种方式来确定当前是双面板还是单面板状态:

        // Determine whether we are in single-pane or dual-pane mode by testing the visibility
        // of the article view.
        View articleView = findViewById(R.id.article);
        mIsDualPane = articleView != null && articleView.getVisibility() == View.VISIBLE;

如果显示新闻内容的view可以找到,而且它处于可见状态,那么说明当前处于双面板状态。mIsDualPane 这个布尔变量是用来确定当前显示模式的一个变量。这里如果是双面板,mIsDualPane 的值为true。

确定了设备大小之后,我们还要根据设备的屏幕进行不同的设置,比如新闻标题是否显示选中,Tab是都显示出来还是显示一个(采用list模式),都可以调用相关方法,然后将mIsDualPane 作为参数穿进去即可。

在实现HeadlinesFragment接口的函数里,我们也要根据mIsDualPane判断,到底是直接改变右侧Fragment的显示内容(平板),还是启动一个独立的Activity去显示新闻内容(手机)。

public void onHeadlineSelected(int index) {
        mArtIndex = index;
        if (mIsDualPane) {
            // display it on the article fragment
            mArticleFragment.displayArticle(mCurrentCat.getArticle(index));
        }
        else {
            // use separate activity
            Intent i = new Intent(this, ArticleActivity.class);
            i.putExtra("catIndex", mCatIndex);
            i.putExtra("artIndex", index);
            startActivity(i);
        }
    }

我觉得通过这个方式,减少了为不同屏幕设备编写APP的代码复杂度,提高了程序的灵活度,很值得学习。何况我刚学,还这么菜。

还有,最后这个Button,在API11之前,也就是没有ActionBar的时候(虽然现在ActionBar官方也不建议使用了),为了低版本的设备也能用类似Tab导航的功能,我们就要引入一个Button来替代ActionBar。在这个例子中,低版本的设备会去加载有Button的布局,而高版本则不会,所以我们只要在代码中检查是否有Button这个实例即可。如果有,说明是低版本,我们给这个Button设置监听,实现类似Tab的效果,如果没有,说明是高版本,就不用操心了。


Tab Listener的灵活使用

我们看一下相关的代码先:

在CompatActionBarNavListener里面只是定义了一个接口:

public interface CompatActionBarNavListener {
    /**
     * Signals that the given news category was selected.
     * @param catIndex the selected category's index.
     */
    public void onCategorySelected(int catIndex);
}

在CompatActionBarNavHandler继承了TabListener和OnNavigationListener,它是Tab的监听类:

public class CompatActionBarNavHandler implements TabListener, OnNavigationListener {
    // The listener that we notify of navigation events
    CompatActionBarNavListener mNavListener;

    /**
     * Constructs an instance with the given listener.
     *
     * @param listener the listener to notify when a navigation event occurs.
     */
    public CompatActionBarNavHandler(CompatActionBarNavListener listener) {
        mNavListener = listener;
    }

    /**
     * Called by framework when a tab is selected.
     *
     * This will cause a navigation event to be delivered to the configured listener.
     */
    @Override
    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        // TODO Auto-generated method stub
        mNavListener.onCategorySelected(tab.getPosition());
    }

    /**
     * Called by framework when a item on the navigation menu is selected.
     *
     * This will cause a navigation event to be delivered to the configured listener.
     */
    @Override
    public boolean onNavigationItemSelected(int itemPosition, long itemId) {
        mNavListener.onCategorySelected(itemPosition);
        return true;
    }


    /**
     * Called by framework when a tab is re-selected. That is, it was already selected and is
     * tapped on again. This is not used in our app.
     */
    @Override
    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // we don't care
    }

    /**
     * Called by framework when a tab is unselected. Not used in our app.
     */
    @Override
    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        // we don't care
    }

}

我们看到CompatActionBarNavHandler主要是通过调用CompatActionBarNavListener接口里的方法来完成监听的,而CompatActionBarNavListener的实现在主Activity里面实现。怎么样,这个套路是否惊人的类似。

没错,在Fragment和Activity通信的时候,它也是这么做的,这么做的原因很简单,降低耦合,我只要定义交互方式,你来定义具体实现。现在可能看不出来好处这种做法的好处,但如果有很多个Activity需要Tab导航,但是需要的具体实现不同,好处就显示出来。因为不是通过硬编码的方式结合起来,所以具体的时候你可以私人订制,提高了代码的灵活度,还没用过的我要记住这种设计思想。

简单贴一下,主Activity相关这个接口的实现:

    @Override
    public void onCategorySelected(int catIndex) {
        setNewsCategory(catIndex);
    }

    void setNewsCategory(int categoryIndex) {
        mCatIndex = categoryIndex;
        mCurrentCat = NewsSource.getInstance().getCategory(categoryIndex);
        mHeadlinesFragment.loadCategory(categoryIndex);

        // If we are displaying the article on the right, we have to update that too
        if (mIsDualPane) {
            mArticleFragment.displayArticle(mCurrentCat.getArticle(0));
        }

        // If we are displaying a "category" button (on the ActionBar-less UI), we have to update
        // its text to reflect the current category.
        Button catButton = (Button) findViewById(R.id.categorybutton);
        if (catButton != null) {
            catButton.setText(CATEGORIES[mCatIndex]);
        }
    }

恩,主Activity通过setNewsCategory()来完成换新闻目录的功能。


功能层层递进,关系分明,没有累述的类定义

这个我在最开始将新闻内容类的时候就提过了。通过一层层实现,每层完成自己分内的任务,设计好每层之间相应的接口,大家一起合作,完成新闻内容的设置。

这是类的好处之一,善于应用Java语言带来的优势,在较短时间开发出功能完备,代码简洁的好程序。


好了,今天的内容就这么多了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值