`
xiaonao880516
  • 浏览: 57204 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

ICS多平台界面支持原理分析

阅读更多
1      简介(Introduction)
Android4.0 推出后,宣布可以同时支持手机,平板,和电视多个平台。

    我们做了如下实验:

在模拟器上用不同设置的android 虚拟设备(AVD) 运行,可得到如下不同的效果。

         创建虚拟设备:

       

        使用此AVD设置,启动模拟器后运行联系人应用,主界面效果图如下:

针对传统的手机设置:

针对此配置的虚拟设备,启动模拟器后运行联系人应用,主界面效果图如下:



可以明显的看到,针对较小的屏幕,android实现了完全不同的效果。在平板模式下,针对大屏幕,设计了左侧列表,右侧详细内容的界面。这样的设计充分利用了屏幕空间,让用户在一屏中看到更多的信息,避免了屏幕看起来空旷。同时也让用户可以直接进行多种操作,包括打电话,发短信等等。简化了用户的操作过程,提升了用户的操作感受。

而在电话屏幕尺寸下,针对较小屏幕的设计方案使得用户界面看起来干净清爽而不拥挤。同时也比较紧凑,同样提供了很好的用户体验。

更重要的是,这两套界面是由同一个应用运行出来的效果。也就是说,只要我们预先规划设计好我们的代码,根据不同的平台(平板、手机)特性,按照一定的方法,设计实现我们的软件产品,就可以实现维护一套而在多个平台实现不同效果,这样能够大大减轻我们的工作量,实现不同平台的快速移植和升级。



2       原理分析
Android 在设计之初,就考虑到支持在不同设备上运行的问题。为应用开发者提供了一套解决

方案。

2.1    基本概念
下面介绍一些Android应用开发中与不同设备展示相关的一些术语:

屏幕尺寸(Screen size)

    屏幕的物理尺寸。 根据屏幕的对角线长度确定。

简而言之,Android把屏幕尺寸分为四大类: 小( small),中等( normal),大(large), 和超大( extra large)。图一表示了屏幕尺寸的划分标准。

屏幕密度(Screen density)

    表示在一定的物理区域内能够显示的点的数目,通常用dpi(dot per inch)作为单位衡量。Dpi 的含义是一英寸之内有多少个点。密度越小的显示设备在固定区域内显示的点数越少。  

通常,Android跟据显示密度把屏幕也分为四种:低(low),中(medium),高(high),超高(extra high)。图一表示了屏幕密度的划分标准。

屏幕方向(Orientation)

    屏幕方向。包括横屏和竖屏。不同的设备有不同的缺省的屏幕方向。而且屏幕方向在运行时可以通过用户的旋转操作而更改。

分辨率(Resolution)

    一屏能显示的物理点数。在开发支持多屏显示的应用过程中,应用程序不应该直接根据屏幕的分辨率来设置。而应该更多考虑屏幕尺寸和密度。

密度无关像素(Density-independent pixel /dp)

    一种虚拟的像素单位,用此单位设计应用程序界面布局可以实现多屏幕支持效果。Dp表示的是一个准确的物理长度。它的大小等于显示密度为160dpi的一个像素的大小。如果在设计应用布局时以dp为单位。Android系统在渲染界面时会根据当前屏幕特显示密度来调整显示大小。有一个计算公式可以表明在不同显示密度屏幕上的像素显示大小为:

    dp = px * (dpi / 160)

    例如: 在显示密度为240dpi的屏幕上,一个dp的显示大小应该为1.5像素。

  

                  图一 android 系统对屏幕尺寸和密度的大概划分



2.2    原理分析
为了实现对多种分辨率和多种显示设备的支持,Android实现了一套机制,可以根据当前设

备的配置自动加载对应的资源文件以实现不同的效果。

2.2.1  提供可选资源
在Anroid 的应用程序开发中,会包含res目录,里面存储着所有应用程序需要的资源文

件,通常包括如下目录:

      \ drawable: 主要保存图标等资源文件。

      \ value: 主要保存字符串等资源文件。

      \ layout:主要保存布局等资源文件。

    针对不同的设备配置,可以通过如下方式定义适合设备的资源文件:

    \drawable-hdpi: 适合高清屏显示的图片资源。

    \layout-large: 适合大屏幕显示的布局文件。

       这样,在将应用程序装在到不同设备室,Android会根据不同的系统配置为应用程序加载对应的资源文件。下一节将描述android选择资源的基本方法。

2.2.2  选择可选资源
本节通过一个例子来说明在运行过程中Android如何选择适合的资源文件:

假定运行设备的配置如下:



Locale = en-GB

Screen orientation = port

Screen pixel density = hdpi

Touchscreen type = notouch

Primary text input method = 12key



而应用程序提供了如下资源文件目录:

drawable/

drawable-en/

drawable-fr-rCA/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/



Android 将按照如下算法流程进行筛选:

         



具体步骤与结果如下所示:

1.       从目录列表中删除与设备配置相冲突的目录。

            drawable-fr-rCA/ 目录被排除, 因为当前区域设置的是en-GB.



drawable/

drawable-en/

drawable-fr-rCA/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/

2.       按照下面链接中表二的次序,依次比较对应的目录。

sdk/android-sdk-linux_x86/docs/guide/topics/resources/providing-resources.html

3.       有某个目录包含表中所列出的限定条件吗?

1)      如果没有,返回2 继续比较。

2)      如果有,则转入4。

4.       排除不包含限定条件的目录项。在本文的例子中,

drawable/

drawable-en/

drawable-en-port/

drawable-en-notouch-12key/

drawable-port-ldpi/

drawable-port-notouch-12key/

标记为红色的三个目录被排除。因为所选择的语言是英语。

5.       重复2,3,4直到只剩下一个目录。在本例中,

drawable-en/

drawable-en-port/

drawable-en-notouch-12key/



最终选择了作为资源文件目录。



2.2.3  Android 3.2的扩展
在Android3.0 开始支持平板。开发平板应用时,需要吧布局相关文件加入res/layout-xlarge/

目录中。但是由于有些情况下,这种按照级别分类的情况并不能很好的支持对平板的支持,例如:对7寸屏和五寸屏的布局按照上面所述的分类标准都应该放置在res/layout-large/。但是,显而易见,这两款屏幕差别甚大,应该使用不同的布局,因此,Android 3.2以后提供了更加精准的支持。

其实现原理是:用户定义每一个布局时,需要声明他可以工作的屏幕的最小尺寸。这样就可以更准确的定义每个布局的工作环境。表示方法如下表所示:



屏幕配置

限定值

描述

smallestWidth

sw<N>dp



例如:

sw600dp

sw720dp

屏幕尺寸限制。描述屏幕的长和宽中最小值需要满足的限制条件。当屏幕发生旋转时,这个属性值不变。

Available screen width

w<N>dp



例如:

w720dp

w1024dp

屏幕宽度需要满足的最小尺寸。

Available screen height

h<N>dp



例如:

h720dp

h1024dp

etc.

屏幕高度需要满足的最小尺寸。

通过这三个变量的描述,可以更加精确的对布局进行限定。下一节将结合代码对如何使用此特性实现支持多平台的应用程序进行分析。





3      模块代码分析


3.1    联系人模块


导入联系人模块代码,用eclipse打开资源目录,看到列表如下:

参照前面的分析,布局文件主要有5个目录:

        分析源代码,当启动联系人第一屏时,运行了PeopleActivity.java。这个activity加载了people_activity.xml文件作为界面布局。而这个布局文件在layout-sw580dp-w1000dp和layout中都有定义。根据前文的分析和验证结果,layout中的布局文件应该是手机界面使用的,而layout-sw580dp-w1000dp中定义的布局文件是平板界面使用的。

        下面是对这一分析结果的验证:

(1)     修改layout 目录下的布局文件,people_activity.xml: 见下文,红色为后来添加的部分:

(2)    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"

(3)        android:id="@+id/list_container"

(4)        android:layout_width="match_parent"

(5)        android:layout_height="match_parent">

(6)       

(7)        <LinearLayout

(8)                android:id="@+id/browse_view"

(9)                android:layout_width="match_parent"

(10)            android:layout_height="match_parent"

(11)            android:orientation="vertical"

(12)            android:background="@drawable/list_background_holo">

(13)          

(14)    <!-- add a text view for test if load this layout file on default phone mode -->

(15)    <TextView

(16)        android:id="@+id/default_text"

(17)        android:text="@string/layout_choose_test"

(18)        android:layout_height="20dp"

(19)        android:layout_width="match_parent"

(20)        />

(21)  

(22)    <Button

(23)        android:id="@+id/change_text_view"

(24)        android:layout_width="match_parent"

(25)        android:layout_height="wrap_content"

(26)        android:text="@string/layout_choose_test" />

(27)

(28)    <!--

(29)        ViewPager for swiping between tabs.  We put StrequentContactListFragment,

(30)        DefaultContactBrowseListFragment and GroupBrowseListFragment at runtime.

(31)

(32)        (Adding them directly as the children of this view is not recommended.  ViewPager should

(33)        be treated like a ListView, which doesn't expect children to be added from the layout.)

(34)    -->

(35)    <android.support.v4.view.ViewPager

(36)        android:id="@+id/tab_pager"

(37)        android:layout_height="match_parent"

(38)        android:layout_width="match_parent"

(39)        />

(40)

(41)

(42)

(43)    <FrameLayout

(44)        android:id="@+id/contacts_unavailable_view"

(45)        android:layout_width="match_parent"

(46)        android:layout_height="match_parent"

(47)        android:visibility="gone">

(48)        <FrameLayout

(49)            android:id="@+id/contacts_unavailable_container"

(50)            android:layout_height="match_parent"

(51)            android:layout_width="match_parent" />

(52)    </FrameLayout>

(53)      </LinearLayout>

(54)</FrameLayout>

运行结果如下图:

     



     显然,运行结果验证了我们的分析。

     同样验证layout-sw580dp-w1000dp下的people_activity.xml:

     布局:

     <FrameLayout

    xmlns:android="http://schemas.android.com/apk/res/android"

    xmlns:ex="http://schemas.android.com/apk/res/com.android.contacts"

    android:layout_width="match_parent"

    android:layout_height="match_parent">



    <com.android.contacts.widget.InterpolatingLayout

        android:id="@+id/main_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:splitMotionEvents="true">



        <LinearLayout

            android:id="@+id/browse_view"

            android:layout_width="wrap_content"

            android:layout_height="match_parent"

            android:orientation="vertical"

            android:minWidth="100dip"

            ex:layout_narrowParentWidth="1000dip"

            ex:layout_narrowWidth="276dip"

            ex:layout_wideParentWidth="1280dip"

            ex:layout_wideWidth="376dip"

            android:background="@drawable/list_background_holo"

            android:visibility="gone">

          

            <TextView

                android:id="@+id/sw580dp_w1000dp"

                android:text="@string/layout_choose_test_sw580dp_w1000dp"

                android:layout_height="20dp"

                android:layout_width="match_parent"

            />

          

            <Button

              android:id="@+id/change_text_view"

              android:layout_width="match_parent"

              android:layout_height="wrap_content"

              android:text="@string/layout_choose_test_sw580dp_w1000dp" />

          



            <!-- All -->

            <fragment

                android:id="@+id/all_fragment"

                class="com.android.contacts.list.DefaultContactBrowseListFragment"

                android:layout_height="0dip"

                android:layout_width="match_parent"

                android:layout_weight="1" />



            <!-- Groups -->

            <fragment

                android:id="@+id/groups_fragment"

                class="com.android.contacts.group.GroupBrowseListFragment"

                android:layout_height="match_parent"

                android:layout_width="match_parent" />

        </LinearLayout>



        <view

            class="com.android.contacts.widget.TransitionAnimationView"

            android:id="@+id/details_view"

            android:layout_width="match_parent"

            android:layout_height="match_parent"

            ex:layout_narrowParentWidth="800dip"

            ex:layout_narrowMarginLeft="0dip"

            ex:layout_narrowMarginRight="0dip"

            ex:layout_wideParentWidth="1280dip"

            ex:layout_wideMarginLeft="0dip"

            ex:layout_wideMarginRight="0dip"

            ex:clipMarginLeft="0dip"

            ex:clipMarginTop="3dip"

            ex:clipMarginRight="3dip"

            ex:clipMarginBottom="9dip"

            ex:enterAnimation="@android:animator/fade_in"

            ex:exitAnimation="@android:animator/fade_out"

            ex:animationDuration="200"

            android:visibility="gone">



            <!-- This layout includes all possible views needed for a contact detail page -->

            <include

                android:id="@+id/contact_detail_container"

                layout="@layout/contact_detail_container"

                android:layout_width="match_parent"

                android:layout_height="match_parent"/>



            <!-- This invisible worker fragment loads the contact's details -->

            <fragment

                android:id="@+id/contact_detail_loader_fragment"

                class="com.android.contacts.detail.ContactLoaderFragment"

                android:layout_height="0dip"

                android:layout_width="0dip"

                android:visibility="gone"/>



            <!-- This is the group detail page -->

            <fragment

                android:id="@+id/group_detail_fragment"

                class="com.android.contacts.group.GroupDetailFragment"

                android:layout_width="match_parent"

                android:layout_height="match_parent"

                android:visibility="gone" />

        </view>



        <view

            class="com.android.contacts.widget.TransitionAnimationView"

            android:id="@+id/favorites_view"

            android:layout_width="match_parent"

            android:layout_height="match_parent"

            ex:clipMarginLeft="0dip"

            ex:clipMarginTop="3dip"

            ex:clipMarginRight="3dip"

            ex:clipMarginBottom="9dip"

            ex:enterAnimation="@android:animator/fade_in"

            ex:exitAnimation="@android:animator/fade_out"

            ex:animationDuration="200">



            <LinearLayout

                android:layout_width="match_parent"

                android:layout_height="match_parent"

                android:background="@drawable/list_background_holo">



                <!-- Starred -->

                <FrameLayout

                    android:layout_width="0dip"

                    android:layout_height="match_parent"

                    android:layout_weight="7"

                    android:background="@drawable/panel_favorites_holo_light">



                    <fragment

                        android:id="@+id/favorites_fragment"

                        class="com.android.contacts.list.ContactTileListFragment"

                        android:layout_height="match_parent"

                        android:layout_width="match_parent"

                        android:layout_marginTop="32dip"

                        android:layout_marginRight="32dip"

                        android:layout_marginLeft="32dip"/>



                </FrameLayout>



                <!-- Most Frequent -->

                <fragment

                    android:id="@+id/frequent_fragment"

                    class="com.android.contacts.list.ContactTileListFragment"

                    android:layout_width="0dip"

                    android:layout_height="match_parent"

                    android:layout_weight="3"

                    android:layout_marginTop="32dip"

                    android:layout_marginRight="16dip"/>



            </LinearLayout>

        </view>



    </com.android.contacts.widget.InterpolatingLayout>



    <com.android.contacts.widget.InterpolatingLayout

        android:id="@+id/contacts_unavailable_view"

        android:layout_width="match_parent"

        android:layout_height="match_parent"

        android:visibility="gone">



        <FrameLayout

            android:id="@+id/contacts_unavailable_container"

            android:layout_height="match_parent"

            android:layout_width="match_parent"

            ex:layout_narrowParentWidth="800dip"

            ex:layout_narrowMarginLeft="80dip"

            ex:layout_narrowMarginRight="80dip"

            ex:layout_wideParentWidth="1280dip"

            ex:layout_wideMarginLeft="200dip"

            ex:layout_wideMarginRight="200dip"

            android:paddingBottom="20dip" />



    </com.android.contacts.widget.InterpolatingLayout>

</FrameLayout>

     用T6的虚拟设备运行新应用,结果如下图所示:



同样和我们的分析结果一致。

关于界面布局的分析和验证结果是一致的,说明,android是根据当前的机器配置,在不同的资源文件目录下选择不同的资源进行加载,从而实现了不同配置下的不同布局。



另外一个问题,由于不同的界面布局导致代码控制逻辑的不同该如何处理呢?

分析联系人代码,发现有如下的控制逻辑:



onCreate -> createViewsAndFragments() {

    if (phoneCapabilityTest.isUsingTwoPanes())

{

    }

    Else

{

}

}



分析phoneCapabilityTest.isUsingTwoPanes()实现,

/* if we are using two-pane layouts ("tablet mode"), false if we are using single views

     * ("phone mode")

     * */

   public static boolean isUsingTwoPanes(Context context) {

return context.getResources().getBoolean(R.bool.config_use_two_panes);

}

显然,从代码注释看,此函数是用于判断当前模式,属于平板还是电话。同样添加代码进行验证。在前面的布局文件中,除了添加一个文本显示控件,还添加了一个按钮。在createViewsAndFragments中添加如下代码:

mTestButton.setOnClickListener(new OnClickListener() {

public void onClick(View v) {

           if (PhoneCapabilityTester.isUsingTwoPanes(PeopleActivity.this)) {

               Log.d("","Click tablet text view");

               mTestTextView = (TextView)findViewById(R.id.sw580dp_w1000dp);                           mTestTextView.setText("Click tablet text view");

           }

           else

           {

                Log.d("","Click tablet phone view");

                mTestTextView = (TextView)findViewById(R.id.default_text);                               mTestTextView.setText("Click phone text view");

            }

       }

});



编译运行,测试结果如下:

首先在手机模式下:

显然,和分析结果一致。

平板测试结果如下:

同样和我们的分析结果一致。

因此,在联系人代码中就是使用isUsingTwoPanes来确定当前的模式。



进一步分析,为什么这个函数就可以判断这两种模式呢?

    public static boolean isUsingTwoPanes(Context context) {

return context.getResources().getBoolean(R.bool.config_use_two_panes);

}

实际上这个函数读取了资源中的一个配置变量config_use_two_panes。它在不同的资源文件中有不同的定义:

res/values-sw580dp/donottranslate_config.xml:  

<bool name="config_use_two_panes">true</bool>

res/values/donottranslate_config.xml:    <bool name="config_use_two_panes">false</bool>

再结合上面对资源目录命名规则的分析,可以清楚的知道,在平板模式下会读取 res/values-sw580dp/donottranslate_config.xml 而电话模式下读取 res/values/donottranslate_config.xml,从而实现了不同的显示模式。

3.2    电话模块
分析电话模块的资源文件目录没有区别手机和平板。因此。电话模块的手机和平板上运行效果是一致的。

经过运行测试也和分析结果一致。



3.3    短信模块
分析短信模块的资源文件目录,可以看到,没有区别手机和平板。因此。短信模块的手机和平板上运行效果是一致的。

经过运行测试也和分析结果一致。

4      如何设计自己的应用程序


结合上面的原理分析以及源码分析,我们如果要实现类似功能,需要做到如下几步:

1.    在设计产品之初,需要根据不同产品给出不同界面定义。此处需要考虑不同界面的功能一致性,以及界面之间的可重用性。

2.    针对产品定义,设计不同的界面布局。通过放置在不同的资源文件目录中,实现应用程序的动态加载。

3.    在应用代码中要针对不同的界面模式实现不同的控制逻辑。
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics