针对多种屏幕进行设计
2016-09-04 20:34
411 查看
前言
从前写项目的时候都是看着我的真机的样子写,写合适我的真机的布局,放适合他的图片啊,心里一直不敢把它放到其他的真机上实验。有一天我鼓起勇气,把它放到其他的手机上运行,哎呀,这是什么啊。完全不是我理想中的样子。而且这还只是手机,我还没有在平板,电脑,电视,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" /> </Relative 1603c Layout>
虽然组件的尺寸有所变化,但它们的空间关系仍会保留。
使用尺寸限定符
灵活的布局优势有限,如果我们要支持更多的设备,给使用不同的设备的用户带来更好的用户体验,我们必须要准备一些针对不同屏幕尺寸的备用布局。这时我们要使用配置限定符,这样系统就能在运行的时候自动选择合适的资源了(根据不同的设备去加载不同的布局文件)。比如很多应用会在大屏幕的设备上实现“双平板模式”(一遍显示标题,另一边显示内容),在平板和电视上可以这么做,但是在手机上就不适合了,所以我们要额外准备文件来应对这种情况:
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来实现:@Override public 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的制作还有一点,可以看这一篇。
好了,今天的内容就是这么多了。
相关文章推荐
- Android之获取手机上的图片和视频缩略图thumbnails
- android wifi 无线调试
- 欧盟法官:对谷歌来说,26 亿美元罚款就像零钱
- Android布局的小窍门?
- Web布局连载——两栏固定布局(五)
- 基于 Linux 的智能手机 Librem 5 开启预售
- 谷歌正式开始补偿Nexus 6P重启门和电池门用户:最高赔400美元
- 5 个可以满足你的生产力、沟通和娱乐需求的开源手机应用
- 每日安全资讯:谷歌发现 G Suite 漏洞,部分密码明文存储长达十四年
- 每日安全资讯:哪些属于App违法违规收集使用个人信息?
- 每日安全资讯:命案侦破过程揭示 Google 能够跟踪全世界的手机
- 一步一步跟我学易语言之第二个易程序菜单设计
- 手机4大隐藏功能及使用必知常识第1/2页
- 谷歌、雅虎支持中文域名搜索 有助提升搜索引擎优化
- 样式表CSS布局经验
- 在winform下实现左右布局多窗口界面的方法之续篇
- 基于逻辑运算的简单权限系统(原理,设计,实现) VBS 版
- css网页布局中注意的几个问题小结
- 51CTO学院新课发布~~带你遇见更好的自己(二))(2017.10.23-10.29)