针对多种屏幕进行设计

来源:互联网 发布:算日子的软件 编辑:程序博客网 时间:2024/05/16 08:15

前言

从前写项目的时候都是看着我的真机的样子写,写合适我的真机的布局,放适合他的图片啊,心里一直不敢把它放到其他的真机上实验。有一天我鼓起勇气,把它放到其他的手机上运行,哎呀,这是什么啊。完全不是我理想中的样子。

而且这还只是手机,我还没有在平板,电脑,电视,wear上看。。不用想也知道,结局肯定惨不忍睹。有幸今天看到了Google这篇针对多种屏幕进行设计的教程,我学到了很多以前根本没接触过的东西。

正文

Android支持多种不同屏幕的设备,手机,平板,电视,手表,所以要让APP兼容不同设备还是很中澳的一件事。但是单是兼容肯定还是远远不够,还要能针对不同的屏幕作出优化,这样才能提升用户的体验。

这次分为三个内容:

  • 支持各种屏幕尺寸
    • 使用灵活的视图尺寸、 RelativeLayout、屏幕尺寸和屏幕方向限定符、别名过滤器以及自动拉伸位图
  • 支持各种屏幕密度
    • 支持具有不同像素密度的屏幕
  • 实施自适应用户界面流程
    • 运行时对当前布局的检测,根据当前布局做出响应,处理屏幕配置变化

支持各种屏幕尺寸

使用“wrap_content”和“match_parent”

要保证布局的灵活性,我们避不开适用这两个。

wrap_content 是能包容下view自身内容的最小尺寸。
match_parent 是延伸view自身去匹配父布局的宽度。

使用它们的值而不是硬编码,view就可以用它们自己的尺寸或填充父布局的尺寸:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">    <LinearLayout android:layout_width="match_parent"                   android:id="@+id/linearLayout1"                    android:gravity="center"                  android:layout_height="50dp">        <ImageView android:id="@+id/imageView1"                    android:layout_height="wrap_content"                   android:layout_width="wrap_content"                   android:src="@drawable/logo"                   android:paddingRight="30dp"                   android:layout_gravity="left"                   android:layout_weight="0" />        <View android:layout_height="wrap_content"               android:id="@+id/view1"              android:layout_width="wrap_content"              android:layout_weight="1" />        <Button android:id="@+id/categorybutton"                android:background="@drawable/button_bg"                android:layout_height="match_parent"                android:layout_weight="0"                android:layout_width="120dp"                style="@style/CategoryButtonStyle"/>    </LinearLayout>    <fragment android:id="@+id/headlines"               android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="match_parent" /></LinearLayout>

此视图在纵向模式和横向模式下的显示效果如下所示。请注意,组件的尺寸会自动适应屏幕的高度和宽度:

这里写图片描述

使用相对布局

你可以使用LinearLayout去构造复杂布局,但LinearLayout只是将view都直接排列出来,如果不想用直线的方式而换用view之间或view和父布局之间的相对关系,那么RelativeLayout是不错的选择,下面是官方示例:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <TextView        android:id="@+id/label"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Type here:"/>    <EditText        android:id="@+id/entry"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_below="@id/label"/>    <Button        android:id="@+id/ok"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_below="@id/entry"        android:layout_alignParentRight="true"        android:layout_marginLeft="10dp"        android:text="OK" />    <Button        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_toLeftOf="@id/ok"        android:layout_alignTop="@id/ok"        android:text="Cancel" /></RelativeLayout>

小屏幕的截图

大屏幕的截图

虽然组件的尺寸有所变化,但它们的空间关系仍会保留。

使用尺寸限定符

灵活的布局优势有限,如果我们要支持更多的设备,给使用不同的设备的用户带来更好的用户体验,我们必须要准备一些针对不同屏幕尺寸的备用布局。这时我们要使用配置限定符,这样系统就能在运行的时候自动选择合适的资源了(根据不同的设备去加载不同的布局文件)。

比如很多应用会在大屏幕的设备上实现“双平板模式”(一遍显示标题,另一边显示内容),在平板和电视上可以这么做,但是在手机上就不适合了,所以我们要额外准备文件来应对这种情况:

res/layout/main.xml,单面板(默认)布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="match_parent" /></LinearLayout>

res/layout-large/main.xml,双面板布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="horizontal">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="400dp"              android:layout_marginRight="10dp"/>    <fragment android:id="@+id/article"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.ArticleFragment"              android:layout_width="fill_parent" /></LinearLayout>

注意到第二个布局文件中文件目录上有large限定符,系统会在较大屏幕上选择该布局,而在小屏幕设备上选择其他布局。

使用最小宽度限定符

在Android版本是3.2之前的设备上,即使设备屏幕大小属于large级别,但是很多应用还是会根据不同的屏幕大小去显示不同的布局(比如说七英寸和十英寸的平板就要用不同的布局),这是最小宽度限定符产生的原因。

最小宽度限定符能让你指定某个最小宽度(单位是dp)来定位屏幕。比如说标准 7 英寸平板电脑的最小宽度为 600 dp,因此如果您要在此类屏幕上的用户界面中使用双面板(但在较小的屏幕上只显示列表),您可以使用上文中所述的单面板和双面板这两种布局,但您应使用 sw600dp 指明双面板布局仅适用于最小宽度为 600 dp 的屏幕,而不是使用 large 尺寸限定符:

res/layout/main.xml,单面板(默认)布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="match_parent" /></LinearLayout>

res/layout-sw600dp/main.xml,双面板布局:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="horizontal">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="400dp"              android:layout_marginRight="10dp"/>    <fragment android:id="@+id/article"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.ArticleFragment"              android:layout_width="fill_parent" /></LinearLayout>

也就是说在系统屏幕大于等于600时,系统会加载layout_sw600dp/main.xml布局,而在手机上会加载layout/main.xml布局。

这个时候问题来了,最小宽度限定符是在Android3.2版本的时候引进的,在3.2版本之前,系统识别不出来sw600dp,只认识large,所以我们不得不去在准备一个尺寸限定符是large的布局文件,而这个布局文件和sw600dp的布局文件的内容完全一样。重复的文件会引起维护的困难,下面会提到如何解决这一问题。

使用布局别名

最小宽度限定符只适用于Android3.2及更高版本,所以我们仍需考虑兼容更低版本的问题。比如我们在手机上显示一个页面,但是在平板和电视上可以显示双面板,我们应该添加如下布局文件:

  • res/layout/main.xml: 单面板布局
  • res/layout-large: 多面板布局
  • res/layout-sw600dp: 多面板布局

后两个文件是相同的,因为其中一个用于和 Android 3.2 设备匹配,而另一个则是为使用较低版本 Android 的平板电脑和电视准备的。

为了避免重复带来维护问题,我们用别名的办法来解决问题。我们定义这两个布局:

  • res/layout/main.xml,单面板布局
  • res/layout/main_twopanes.xml,双面板布局

然后添加这两个文件:

  • res/values-large/layout.xml:
<resources>    <item name="main" type="layout">@layout/main_twopanes</item></resources>
  • res/values-sw600dp/layout.xml:
<resources>    <item name="main" type="layout">@layout/main_twopanes</item></resources>

后面两个的布局我们通过选择器的方式制定设备该加载的布局,Android版本3.2之前认识large,加载了main-twopanes.xml,Android3.2之后的识别到sw600dp,也加载相同的布局。这样通过选择器避开重复定义布局,但都指向了相同的布局,达到了只用定义一个布局的目的。

使用方向限定符

某些布局同时支持横向和纵向显示,但是我们仍然可以通过调整来进一步优化用户体验。官方给出了下面的示例:

  • 小屏幕,纵向:单面板,带徽标
  • 小屏幕,横向:单面板,带徽标
  • 7 英寸平板电脑,纵向:单面板,带操作栏
  • 7 英寸平板电脑,横向:双面板,宽,带操作栏
  • 10 英寸平板电脑,纵向:双面板,窄,带操作栏
  • 10 英寸平板电脑,横向:双面板,宽,带操作栏
  • 电视,横向:双面板,宽,带操作栏

这些布局都定义在res/layout的某个xml文件里,我们可以使用定义多个布局,为了避免重复我们应该用别名来匹配。

res/layout/onepane.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="match_parent" /></LinearLayout>

res/layout/onepane_with_bar.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">    <LinearLayout android:layout_width="match_parent"                   android:id="@+id/linearLayout1"                    android:gravity="center"                  android:layout_height="50dp">        <ImageView android:id="@+id/imageView1"                    android:layout_height="wrap_content"                   android:layout_width="wrap_content"                   android:src="@drawable/logo"                   android:paddingRight="30dp"                   android:layout_gravity="left"                   android:layout_weight="0" />        <View android:layout_height="wrap_content"               android:id="@+id/view1"              android:layout_width="wrap_content"              android:layout_weight="1" />        <Button android:id="@+id/categorybutton"                android:background="@drawable/button_bg"                android:layout_height="match_parent"                android:layout_weight="0"                android:layout_width="120dp"                style="@style/CategoryButtonStyle"/>    </LinearLayout>    <fragment android:id="@+id/headlines"               android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="match_parent" /></LinearLayout>

res/layout/twopanes.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="horizontal">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="400dp"              android:layout_marginRight="10dp"/>    <fragment android:id="@+id/article"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.ArticleFragment"              android:layout_width="fill_parent" /></LinearLayout>

res/layout/twopanes_narrow.xml:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="horizontal">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="200dp"              android:layout_marginRight="10dp"/>    <fragment android:id="@+id/article"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.ArticleFragment"              android:layout_width="fill_parent" /></LinearLayout>

定义好所有可能的布局之后,我们用配置限定符正确映射不同屏幕设配要加载的资源。还是利用别名就能解决。

res/values/layouts.xml:

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

res/values-sw600dp-land/layouts.xml:

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

res/values-sw600dp-port/layouts.xml:

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

res/values-large-land/layouts.xml:

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

res/values-large-port/layouts.xml:

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

使用自动拉伸位图

完成的布局的准备,千万别忘了图片。如果我们在不同大小的设备上使用简单的图片,会发现效果很不理想。因为系统运行的时候会自动的拉伸或是收缩图片。解决的方案是使用自动拉伸位图,这是一种特使格式的PNG图片,其中会指明可以拉伸以及不可以拉伸的区域。

因此,如果设计的是用于尺寸可变的组件上的位图,请务必使用自动拉伸技术。要将某个位图转换成自动拉伸位图,先准备好位图,下面是官方示例:

这里写图片描述

然后通过 SDK 的 draw9patch 实用工具(位于 tools/ 目录中)运行该图片,在该工具中绘制像素以标出要拉伸的区域以及左侧和顶部的边界,还可以沿右侧和底部边界绘制像素以标出用于放置内容的区域

这里写图片描述

请注意沿边界显示的黑色像素。顶部和左侧边界上的像素用于指定可以拉伸的图片区域,右侧和底部边界上的像素则用于指定放置内容的区域。

另请注意 .9.png 的扩展名。因为系统框架需要通过此扩展名确定相关图片是自动拉伸位图,而不是普通 PNG 图片。

如果您将此背景应用到某个组件(通过设置 android:background=”@drawable/button”),系统框架就会正确拉伸图片以适应按钮的尺寸。

这里写图片描述


支持各种屏幕密度

使用非密度制约像素

我们一定要避免使用绝对布局来定义布局或尺寸,因为在不同屏幕密度的设备上运行起来布局尺寸天差地别。所以务必使用dp或sp单位定义尺寸。dp是一种非密度制约像素,其大小和160dpi像素大小相同,sp也是一种基本单位,但他可根据用户的偏好文字大小进行调整,我们应该使用该单位来定义文字大小,不用用它来定义布局啊。

例如,请使用 dp(而非 px)指定两个视图间的间距:

<Button android:layout_width="wrap_content"    android:layout_height="wrap_content"    android:text="@string/clickme"    android:layout_marginTop="20dp" />

请务必使用 sp 指定文字大小:

<TextView android:layout_width="match_parent"    android:layout_height="wrap_content"    android:textSize="20sp" />

提供备用位图

Android可以在不同屏幕尺寸的设备上运行,所以我们应该提供满足普片密度范围的要求:低密度,中密度,高密度以及超高密度,这有助于图片在不同设备上都得到出色的质量和效果。

要提供这些资源,应该先提取元素图片,然后再根据以下尺寸范围为各密度生成图片:

  • xhdpi:2.0
  • hdpi:1.5
  • mdpi:1.0(最低要求)
  • ldpi:0.75

也就是说如果你为xhdpi设备生成了一张200*200尺寸的图片,你要为hdpi提供150*150尺寸的图片,mdpi是100*100,ldpi是75*75。

然后把图片方法相应的子目录下,然后系统会根据设备的屏幕密度去自动选择要加载的图片:

MyProject/  res/    drawable-xhdpi/        awesomeimage.png    drawable-hdpi/        awesomeimage.png    drawable-mdpi/        awesomeimage.png    drawable-ldpi/        awesomeimage.png

实施自适应用户界面流程

运行的时候,设备屏幕大小不同加载的布局文件也不同,这是我们要根据加载的布局来给出相应的响应。

确定当前布局

要确定相应布局的实施,当然要知道现在加载的哪种布局。比如我们想知道用户现在是出于单面板模式还是双面板模式,要做到这一点,我们可以查询视图是否存现以及视图有没有已经显示出来。

public class NewsReaderActivity extends FragmentActivity {    boolean mIsDualPane;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.main_layout);        View articleView = findViewById(R.id.article);        mIsDualPane = articleView != null &&                        articleView.getVisibility() == View.VISIBLE;    }}

再举一个适应各种组件的存在情况的方法示例:在对这些组件执行操作前先查看它们是否可用。新闻阅读器示例应用中有一个用于打开菜单的按钮,但只有在版本低于 3.0 的 Android 上运行该应用时,这个按钮才会存在,因为 API 级别 11 或更高级别中的 ActionBar 已取代了该按钮的功能。因此,可以使用以下代码为此按钮添加事件侦听器:

Button catButton = (Button) findViewById(R.id.categorybutton);OnClickListener listener = /* create your listener here */;if (catButton != null) {    catButton.setOnClickListener(listener);}

根据当前的布局作出响应

因为布局的不同一些操作会产生不同的结果。比如双面板模式,点击点击左侧标题,右侧就能显示文章,而在单面板模式里,就要启动一个独立的Activity或是替换fragment来实现:

@Overridepublic void onHeadlineSelected(int index) {    mArtIndex = index;    if (mIsDualPane) {        /* display article on the right pane */        mArticleFragment.displayArticle(mCurrentCat.getArticle(index));    } else {        /* start a separate activity */        Intent intent = new Intent(this, ArticleActivity.class);        intent.putExtra("catIndex", mCatIndex);        intent.putExtra("artIndex", index);        startActivity(intent);    }}

同样,如果该应用处于双面板模式下,就应设置带导航标签的操作栏;但如果该应用处于单面板模式下,就应使用旋转窗口小部件设置导航栏。

final String CATEGORIES[] = { "热门报道", "政治", "经济", "Technology" };public void onCreate(Bundle savedInstanceState) {    ....    if (mIsDualPane) {        /* use tabs for navigation */        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_TABS);        int i;        for (i = 0; i < CATEGORIES.length; i++) {            actionBar.addTab(actionBar.newTab().setText(                CATEGORIES[i]).setTabListener(handler));        }        actionBar.setSelectedNavigationItem(selTab);    }    else {        /* use list navigation (spinner) */        actionBar.setNavigationMode(android.app.ActionBar.NAVIGATION_MODE_LIST);        SpinnerAdapter adap = new ArrayAdapter(this,                R.layout.headline_item, CATEGORIES);        actionBar.setListNavigationCallbacks(adap, handler);    }}

重复使用其他Avtivity中的代码片段

多屏设计的重复模式是指,对于大屏幕的配置来说,已实施界面的一部分会作为面板,而对于小屏幕设备,这部分可能就是一个独立的Activity。但是因为他们的内容都一样,所以我们应该复用同样的代码片段来避免重复,通常都是复用Fragment来做到这一点。比如用于显示新闻内容的ArticleFragment,在双面板里作为一部分:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="fill_parent"    android:layout_height="fill_parent"    android:orientation="horizontal">    <fragment android:id="@+id/headlines"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.HeadlinesFragment"              android:layout_width="400dp"              android:layout_marginRight="10dp"/>    <fragment android:id="@+id/article"              android:layout_height="fill_parent"              android:name="com.example.android.newsreader.ArticleFragment"              android:layout_width="fill_parent" /></LinearLayout>

在小屏幕设备的Activity里(无需布局)又重复使用了它:

ArticleFragment frag = new ArticleFragment();getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();

当然这个XML文件里面定义这个Fragemnt的效果是相同的,但是我们没有必要去定义,因为这个Fragement是这个Activity里的唯一组件。

当然了,我们在设计Fragment的时候应该要注意不要把它和Activity之间设计的耦合性太强,分都分不开是不好设计。我们只要在Fragment里面设计好它和Activity之间的交互方式,然后去实施它就好了:
比如定义一个接口,而不是直接嵌套在Activity里面:

public class HeadlinesFragment extends ListFragment {    ...    OnHeadlineSelectedListener mHeadlineSelectedListener = null;    /* Must be implemented by host activity */    public interface OnHeadlineSelectedListener {        public void onHeadlineSelected(int index);    }    ...    public void setOnHeadlineSelectedListener(OnHeadlineSelectedListener listener) {        mHeadlineSelectedListener = listener;    }}

然后,如果用户选择某个标题,相关Fragment就会通知由主Activity指定的侦听器(而不是通知某个硬编码的具体Activity):

public class HeadlinesFragment extends ListFragment {    ...    @Override    public void onItemClick(AdapterView<?> parent,                            View view, int position, long id) {        if (null != mHeadlineSelectedListener) {            mHeadlineSelectedListener.onHeadlineSelected(position);        }    }    ...}

处理屏幕配置的变化

如果我们使用单独的Activity作为界面的实施部分,可能要考虑这方面问题。意思是竖直方向上我们是单独的Activity来解决问题的,但是横向显示的时候就转换成双面板模式。这个问题在平板上考虑的更多。

例如,在运行 Android 3.0 或更高版本的标准 7 英寸平板电脑上,如果新闻阅读器示例应用运行在纵向模式下,就会在使用独立活动显示新闻报道;但如果该应用运行在横向模式下,就会使用双面板布局。

如果用户处于纵向模式下且屏幕上显示的是用于阅读报道的活动,那么我们就需要在检测到屏幕方向变化(变成横向模式)后执行相应操作,即停止上述活动并返回主活动,以便在双面板布局中显示相关内容:

public class ArticleActivity extends FragmentActivity {    int mCatIndex, mArtIndex;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        mCatIndex = getIntent().getExtras().getInt("catIndex", 0);        mArtIndex = getIntent().getExtras().getInt("artIndex", 0);        // If should be in two-pane mode, finish to return to main activity        if (getResources().getBoolean(R.bool.has_two_panes)) {            finish();            return;        }        ...}

ps : 中间我查了查drawable和mipmap文件的区别,大家说啥的都由,官方一句概括,应用LOGO在放在mipmap里,提高渲染速度,减少CPU压力,其他的还是放在drawable里和原来一样。我也不太懂,但我是都放在mipmap里的。

ps:如果大家对.9.png的制作还有一点,可以看这一篇。

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

0 0
原创粉丝点击