使用自动拉伸位图
支持各种屏幕尺寸通常意味着您的图片资源还必须能适应各种尺寸。例如,无论要应用到什么形状的按钮上,按钮背景都必须能适应。
如果在可以更改尺寸的组件上使用了简单的图片,您很快就会发现显示效果多少有些不太理想,因为系统会在运行时平均地拉伸或收缩您的图片。解决方法为使用自动拉伸位图,这是一种格式特殊的 PNG 文件,其中会指明可以拉伸以及不可以拉伸的区域。
.9的制作,实际上就是在原图片上添加1px的边界,然后按照我们的需求,把对应的位置设置成黑色线,系统就会根据我们的实际需求进行拉伸。
下图是对.9图的四边的含义的解释,左上边代表拉伸区域,右下边代表padding box,就是间隔区域,在下面,我们给出一个例子,方便大家理解。
先看下面两张图,我们理解一下这四条线的含义。
上图和下图的区别,就在于右下边的黑线不一样,具体的效果的区别,看右边的效果图。上图效果图中深蓝色的区域,代表内容区域,我们可以看到是在正中央的,这是因为我们在右下边的是两个点,这两个点距离上下左右四个方向的距离就是padding的距离,所以深蓝色内容区域在图片正中央,我们再看下图,由于右下边的黑线是图片长度,所以就没有padding,从效果图上的表现就是深蓝色区域和图片一样大,因此,我们可以利用右下边来控制内容与背景图边缘的padding。
如果你还不明白,那么我们看下面的效果图,我们分别以图一和图二作为背景图,下面是效果图。
我们可以看到,使用wrap_content属性设置长宽,图一比图二的效果大一圈,这是为什么呢?还记得我上面说的padding吗?
这就是padding的效果提现,怎么证明呢?我们再看下面一张图,给图一添加padding=0,这样背景图设置的padding效果就没了,是不是两个一样大了?
ok,我想你应该明白右下边的黑线的含义了,下面我们再看一下左上边的效果。
下面我们只设置了左上边线,效果图如下
上面的线没有包住图标,下面的线正好包住了图标,从右边的效果图应该可以看出差别,黑线所在的区域就是拉伸区域,上图黑线所在的全是纯色,所以图标不变形,下面的拉伸区域包裹了图标,所以在拉伸的时候就会对图标进行拉伸,但是这样就会导致图标变形。注意到下面红线区域了嘛?这是系统提示我们的,因为这样拉伸,不符合要求,所以会提示一下。
支持各种屏幕密度
使用非密度制约像素
由于各种屏幕的像素密度都有所不同,因此相同数量的像素在不同设备上的实际大小也有所差异,这样使用像素定义布局尺寸就会产生问题。因此,请务必使用 dp 或 sp 单位指定尺寸。dp 是一种非密度制约像素,其尺寸与 160 dpi 像素的实际尺寸相同。sp 也是一种基本单位,但它可根据用户的偏好文字大小进行调整(即尺度独立性像素),因此我们应将该测量单位用于定义文字大小。
例如,请使用 dp(而非 px)指定两个视图间的间距:
[html] view
plain copy
- <Button android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/clickme"
- android:layout_marginTop="20dp" />
请务必使用 sp 指定文字大小:
[html] view
plain copy
- <TextView android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:textSize="20sp" />
除了介绍这些最基础的知识之外,我们下面再来讨论一下另外一个问题。
经过上面的介绍,我们都清楚,为了能够规避不同像素密度的陷阱,Google推荐使用dp来代替px作为控件长度的度量单位,但是我们来看下面的一个场景。
假如我们以Nexus5作为书写代码时查看效果的测试机型,Nexus5的总宽度为360dp,我们现在需要在水平方向上放置两个按钮,一个是150dp左对齐,另外一个是200dp右对齐,中间留有10dp间隔,那么在Nexus5上面的显示效果就是下面这样
但是如果在Nexus S或者是Nexus One运行呢?下面是运行结果
可以看到,两个按钮发生了重叠。
我们都已经用了dp了,为什么会出现这种情况呢?
你听我慢慢道来。
虽然说dp可以去除不同像素密度的问题,使得1dp在不同像素密度上面的显示效果相同,但是还是由于Android屏幕设备的多样性,如果使用dp来作为度量单位,并不是所有的屏幕的宽度都是相同的dp长度,比如说,Nexus S和Nexus One属于hdpi,屏幕宽度是320dp,而Nexus 5属于xxhdpi,屏幕宽度是360dp,Galaxy Nexus属于xhdpi,屏幕宽度是384dp,Nexus 6 属于xxxhdpi,屏幕宽度是410dp。所以说,光Google自己一家的产品就已经有这么多的标准,而且屏幕宽度和像素密度没有任何关联关系,即使我们使用dp,在320dp宽度的设备和410dp的设备上,还是会有90dp的差别。当然,我们尽量使用match_parent和wrap_content,尽可能少的用dp来指定控件的具体长宽,再结合上权重,大部分的情况我们都是可以做到适配的。
但是除了这个方法,我们还有没有其他的更彻底的解决方案呢?
我们换另外一个思路来思考这个问题。
下面的方案来自Android Day Day Up 一群的【blue-深圳】,谢谢他的分享精神
因为分辨率不一样,所以不能用px;因为屏幕宽度不一样,所以要小心的用dp,那么我们可不可以用另外一种方法来统一单位,不管分辨率是多大,屏幕宽度用一个固定的值的单位来统计呢?
答案是:当然可以。
我们假设手机屏幕的宽度都是320某单位,那么我们将一个屏幕宽度的总像素数平均分成320份,每一份对应具体的像素就可以了。
具体如何来实现呢?我们看下面的代码
[java] view
plain copy
- import java.io.File;
- import java.io.FileNotFoundException;
- import java.io.FileOutputStream;
- import java.io.PrintWriter;
- public class MakeXml {
- private final static String rootPath = "C:\\Users\\Administrator\\Desktop\\layoutroot\\values-{0}x{1}\\";
- private final static float dw = 320f;
- private final static float dh = 480f;
- private final static String WTemplate = "<dimen name=\"x{0}\">{1}px</dimen>\n";
- private final static String HTemplate = "<dimen name=\"y{0}\">{1}px</dimen>\n";
- public static void main(String[] args) {
- makeString(320, 480);
- makeString(480,800);
- makeString(480, 854);
- makeString(540, 960);
- makeString(600, 1024);
- makeString(720, 1184);
- makeString(720, 1196);
- makeString(720, 1280);
- makeString(768, 1024);
- makeString(800, 1280);
- makeString(1080, 1812);
- makeString(1080, 1920);
- makeString(1440, 2560);
- }
- public static void makeString(int w, int h) {
- StringBuffer sb = new StringBuffer();
- sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
- sb.append("<resources>");
- float cellw = w / dw;
- for (int i = 1; i < 320; i++) {
- sb.append(WTemplate.replace("{0}", i + "").replace("{1}",
- change(cellw * i) + ""));
- }
- sb.append(WTemplate.replace("{0}", "320").replace("{1}", w + ""));
- sb.append("</resources>");
- StringBuffer sb2 = new StringBuffer();
- sb2.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
- sb2.append("<resources>");
- float cellh = h / dh;
- for (int i = 1; i < 480; i++) {
- sb2.append(HTemplate.replace("{0}", i + "").replace("{1}",
- change(cellh * i) + ""));
- }
- sb2.append(HTemplate.replace("{0}", "480").replace("{1}", h + ""));
- sb2.append("</resources>");
- String path = rootPath.replace("{0}", h + "").replace("{1}", w + "");
- File rootFile = new File(path);
- if (!rootFile.exists()) {
- rootFile.mkdirs();
- }
- File layxFile = new File(path + "lay_x.xml");
- File layyFile = new File(path + "lay_y.xml");
- try {
- PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
- pw.print(sb.toString());
- pw.close();
- pw = new PrintWriter(new FileOutputStream(layyFile));
- pw.print(sb2.toString());
- pw.close();
- } catch (FileNotFoundException e) {
- e.printStackTrace();
- }
- }
- public static float change(float a) {
- int temp = (int) (a * 100);
- return temp / 100f;
- }
- }
代码应该很好懂,我们将一个屏幕宽度分为320份,高度480份,然后按照实际像素对每一个单位进行复制,放在对应values-widthxheight文件夹下面的lax.xml和lay.xml里面,这样就可以统一所有你想要的分辨率的单位了,下面是生成的一个320*480分辨率的文件,因为宽高分割之后总分数和像素数相同,所以x1就是1px,以此类推
[html] view
plain copy
- <?xml version="1.0" encoding="utf-8"?>
- <resources><dimen name="x1">1.0px</dimen>
- <dimen name="x2">2.0px</dimen>
- <dimen name="x3">3.0px</dimen>
- <dimen name="x4">4.0px</dimen>
- <dimen name="x5">5.0px</dimen>
- <dimen name="x6">6.0px</dimen>
- <dimen name="x7">7.0px</dimen>
- <dimen name="x8">8.0px</dimen>
- <dimen name="x9">9.0px</dimen>
- <dimen name="x10">10.0px</dimen>
- ...省略好多行
- <dimen name="x300">300.0px</dimen>
- <dimen name="x301">301.0px</dimen>
- <dimen name="x302">302.0px</dimen>
- <dimen name="x303">303.0px</dimen>
- <dimen name="x304">304.0px</dimen>
- <dimen name="x305">305.0px</dimen>
- <dimen name="x306">306.0px</dimen>
- <dimen name="x307">307.0px</dimen>
- <dimen name="x308">308.0px</dimen>
- <dimen name="x309">309.0px</dimen>
- <dimen name="x310">310.0px</dimen>
- <dimen name="x311">311.0px</dimen>
- <dimen name="x312">312.0px</dimen>
- <dimen name="x313">313.0px</dimen>
- <dimen name="x314">314.0px</dimen>
- <dimen name="x315">315.0px</dimen>
- <dimen name="x316">316.0px</dimen>
- <dimen name="x317">317.0px</dimen>
- <dimen name="x318">318.0px</dimen>
- <dimen name="x319">319.0px</dimen>
- <dimen name="x320">320px</dimen>
- </resources>
那么1080*1960分辨率下是什么样子呢?我们可以看下,由于1080和320是3.37倍的关系,所以x1=3.37px
[html] view
plain copy
- <?xml version="1.0" encoding="utf-8"?>
- <resources><dimen name="x1">3.37px</dimen>
- <dimen name="x2">6.75px</dimen>
- <dimen name="x3">10.12px</dimen>
- <dimen name="x4">13.5px</dimen>
- <dimen name="x5">16.87px</dimen>
- <dimen name="x6">20.25px</dimen>
- <dimen name="x7">23.62px</dimen>
- <dimen name="x8">27.0px</dimen>
- <dimen name="x9">30.37px</dimen>
- <dimen name="x10">33.75px</dimen>
- ...省略好多行
- <dimen name="x300">1012.5px</dimen>
- <dimen name="x301">1015.87px</dimen>
- <dimen name="x302">1019.25px</dimen>
- <dimen name="x303">1022.62px</dimen>
- <dimen name="x304">1026.0px</dimen>
- <dimen name="x305">1029.37px</dimen>
- <dimen name="x306">1032.75px</dimen>
- <dimen name="x307">1036.12px</dimen>
- <dimen name="x308">1039.5px</dimen>
- <dimen name="x309">1042.87px</dimen>
- <dimen name="x310">1046.25px</dimen>
- <dimen name="x311">1049.62px</dimen>
- <dimen name="x312">1053.0px</dimen>
- <dimen name="x313">1056.37px</dimen>
- <dimen name="x314">1059.75px</dimen>
- <dimen name="x315">1063.12px</dimen>
- <dimen name="x316">1066.5px</dimen>
- <dimen name="x317">1069.87px</dimen>
- <dimen name="x318">1073.25px</dimen>
- <dimen name="x319">1076.62px</dimen>
- <dimen name="x320">1080px</dimen>
- </resources>
无论在什么分辨率下,x320都是代表屏幕宽度,y480都是代表屏幕高度。
那么,我们应该如何使用呢?
首先,我们要把生成的所有values文件夹放到res目录下,当设计师把UI高清设计图给你之后,你就可以根据设计图上的尺寸,以某一个分辨率的机型为基础,找到对应像素数的单位,然后设置给控件即可。
下图还是两个Button,不同的是,我们把单位换成了我们在values文件夹下dimen的值,这样在你指定的分辨率下,不管宽度是320dp、360dp,还是410dp,就都可以完全适配了。
但是,还是有个问题,为什么下面的三个没有适配呢?
这是因为由于在生成的values文件夹里,没有对应的分辨率,其实一开始是报错的,因为默认的values没有对应dimen,所以我只能在默认values里面也创建对应文件,但是里面的数据却不好处理,因为不知道分辨率,我只好默认为x1=1dp保证尽量兼容。这也是这个解决方案的几个弊端,对于没有生成对应分辨率文件的手机,会使用默认values文件夹,如果默认文件夹没有,就会出现问题。
所以说,这个方案虽然是一劳永逸,但是由于实际上还是使用的px作为长度的度量单位,所以多少和google的要求有所背离,不好说以后会不会出现什么不可预测的问题。其次,如果要使用这个方案,你必须尽可能多的包含所有的分辨率,因为这个是使用这个方案的基础,如果有分辨率缺少,会造成显示效果很差,甚至出错的风险,而这又势必会增加软件包的大小和维护的难度,所以大家自己斟酌,择优使用。
更多信息可参考鸿洋的新文章Android 屏幕适配方案
提供备用位图
由于 Android 可在具有各种屏幕密度的设备上运行,因此我们提供的位图资源应始终可以满足各类普遍密度范围的要求:低密度、中等密度、高密度以及超高密度。这将有助于我们的图片在所有屏幕密度上都能得到出色的质量和效果。
要生成这些图片,我们应先提取矢量格式的原始资源,然后根据以下尺寸范围针对各密度生成相应的图片。
- xhdpi:2.0
- hdpi:1.5
- mdpi:1.0(最低要求)
- ldpi:0.75
也就是说,如果我们为 xhdpi 设备生成了 200x200 px尺寸的图片,就应该使用同一资源为 hdpi、mdpi 和 ldpi 设备分别生成 150x150、100x100 和 75x75 尺寸的图片。
然后,将生成的图片文件放在 res/ 下的相应子目录中(mdpi、hdpi、xhdpi、xxhdpi),系统就会根据运行您应用的设备的屏幕密度自动选择合适的图片。
这样一来,只要我们引用 @drawable/id,系统都能根据相应屏幕的 dpi 选取合适的位图。
还记得我们上面提到的图标设计尺寸吗?和这个其实是一个意思。
但是还有个问题需要注意下,如果是.9图或者是不需要多个分辨率的图片,就放在drawable文件夹即可,对应分辨率的图片要正确的放在合适的文件夹,否则会造成图片拉伸等问题。
实施自适应用户界面流程
前面我们介绍过,如何根据设备特点显示恰当的布局,但是这样做,会使得用户界面流程可能会有所不同。例如,如果应用处于双面板模式下,点击左侧面板上的项即可直接在右侧面板上显示相关内容;而如果该应用处于单面板模式下,点击相关的内容应该跳转到另外一个Activity进行后续的处理。所以我们应该按照下面的流程,一步步的完成自适应界面的实现。
确定当前布局
由于每种布局的实施都会稍有不同,因此我们需要先确定当前向用户显示的布局。例如,我们可以先了解用户所处的是“单面板”模式还是“双面板”模式。要做到这一点,可以通过查询指定视图是否存在以及是否已显示出来。
[java] view
plain copy
- 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 已取代了该按钮的功能。因此,您可以使用以下代码为此按钮添加事件侦听器:
[java] view
plain copy
- Button catButton = (Button) findViewById(R.id.categorybutton);
- OnClickListener listener = /* create your listener here */;
- if (catButton != null) {
- catButton.setOnClickListener(listener);
- }
根据当前布局做出响应
有些操作可能会因当前的具体布局而产生不同的结果。例如,在新闻阅读器示例中,如果用户界面处于双面板模式下,那么点击标题列表中的标题就会在右侧面板中打开相应报道;但如果用户界面处于单面板模式下,那么上述操作就会启动一个独立活动:
[java] view
plain copy
- @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);
- }
- }
同样,如果该应用处于双面板模式下,就应设置带导航标签的操作栏;但如果该应用处于单面板模式下,就应使用下拉菜单设置导航栏。因此我们的代码还应确定哪种情况比较合适:
[java] view
plain copy
- 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);
- }
- }
重复使用其他活动中的片段
多屏幕设计中的重复模式是指,对于某些屏幕配置,已实施界面的一部分会用作面板;但对于其他配置,这部分就会以独立活动的形式存在。例如,在新闻阅读器示例中,对于较大的屏幕,新闻报道文本会显示在右侧面板中;但对于较小的屏幕,这些文本就会以独立活动的形式存在。
在类似情况下,通常可以在多个活动中重复使用相同的 Fragment 子类以避免代码重复。例如,在双面板布局中使用了 ArticleFragment:
[html] view
plain copy
- <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布局中重复使用了它 :
[java] view
plain copy
- ArticleFragment frag = new ArticleFragment();
- getSupportFragmentManager().beginTransaction().add(android.R.id.content, frag).commit();
当然,这与在 XML 布局中声明片段的效果是一样的,但在这种情况下却没必要使用 XML 布局,因为报道片段是此活动中的唯一组件。
请务必在设计片段时注意,不要针对具体活动创建强耦合。要做到这一点,通常可以定义一个接口,该接口概括了相关片段与其主活动交互所需的全部方式,然后让主活动实施该界面:
例如,新闻阅读器应用的 HeadlinesFragment 会精确执行以下代码:
[java] view
plain copy
- 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;
- }
- }
然后,如果用户选择某个标题,相关片段就会通知由主活动指定的侦听器(而不是通知某个硬编码的具体活动):
[java] view
plain copy
- public class HeadlinesFragment extends ListFragment {
- ...
- @Override
- public void onItemClick(AdapterView<?> parent,
- View view, int position, long id) {
- if (null != mHeadlineSelectedListener) {
- mHeadlineSelectedListener.onHeadlineSelected(position);
- }
- }
- ...
- }
除此之外,我们还可以使用第三方框架,比如说使用“订阅-发布”模式的EventBus来更多的优化组件之间的通信,减少耦合。
处理屏幕配置变化
如果我们使用独立Activity实施界面的独立部分,那么请注意,我们可能需要对特定配置变化(例如屏幕方向的变化)做出响应,以便保持界面的一致性。
例如,在运行 Android 3.0 或更高版本的标准 7 英寸平板电脑上,如果新闻阅读器示例应用运行在纵向模式下,就会在使用独立活动显示新闻报道;但如果该应用运行在横向模式下,就会使用双面板布局。
也就是说,如果用户处于纵向模式下且屏幕上显示的是用于阅读报道的活动,那么就需要在检测到屏幕方向变化(变成横向模式)后执行相应操作,即停止上述活动并返回主活动,以便在双面板布局中显示相关内容:
[java] view
plain copy
- 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;
- }
- ...
- }
通过上面几个步骤,我们就完全可以建立一个可以根据用户界面配置进行自适应的App了。
最佳实践
关于高清设计图尺寸
Google官方给出的高清设计图尺寸有两种方案,一种是以mdpi设计,然后对应放大得到更高分辨率的图片,另外一种则是以高分辨率作为设计大小,然后按照倍数对应缩小到小分辨率的图片。
根据经验,我更推荐第二种方法,因为小分辨率在生成高分辨率图片的时候,会出现像素丢失,我不知道是不是有方法可以阻止这种情况发生。
而分辨率可以以1280*720或者是1960*1080作为主要分辨率进行设计。
ImageView的ScaleType属性
设置不同的ScaleType会得到不同的显示效果,一般情况下,设置为centerCrop能获得较好的适配效果。
动态设置
有一些情况下,我们需要动态的设置控件大小或者是位置,比如说popwindow的显示位置和偏移量等,这个时候我们可以动态的获取当前的屏幕属性,然后设置合适的数值
[java] view
plain copy
- public class ScreenSizeUtil {
- public static int getScreenWidth(Activity activity) {
- return activity.getWindowManager().getDefaultDisplay().getWidth();
- }
- public static int getScreenHeight(Activity activity) {
- return activity.getWindowManager().getDefaultDisplay().getHeight();
- }
- }