解决方案-支持各种屏幕密度

基本概念

  • px: 像素(pixel),设备上的物理像素点。
  • dp:跟dip是同一个概念,与屏幕物理像素点无关,而是基于屏幕密度抽象单位,被称作“设备独立像素”,会随着屏幕的密度进行自动的大小调整。
  • sp: 专用于文字大小的设置,可根据字体大小首选项进行缩放。
  • inch: 英寸,1英寸 = 2.54厘米。
  • dpi: 屏幕像素密度, 单位英寸上像素点的数量。
  • 分辨率: 是指横纵方向上的像素点数,单位是px。一般是纵向像素*横向像素,比如1920 * 1080
  • 屏幕尺寸: 屏幕对角线的长度,单位是inch。比如我们平时常说的5寸手机,是指手机屏幕的对角线长度为5英寸。

转换关系

常见尺寸表

level dpi scale resolution
ldpi 120 0.75 320*240
mdpi 160 1 480*320
hdpi 240 1.5 800*480
xhdpi 320 2 1280*720
xxhdpi 480 3 1920*1080
xxxhdpi 640 4 2560*1440

说明:

  • google官方用的词是approximately,比如ldpi是值近似等于120dpi;
  • 分辨率列只是比较有代表性的而已,但设备的dpi,还需要根据相应的设备尺寸来计算。同一分辨率,设备尺寸越大,那么dpi就越小,单位英寸上像素点越稀疏。

换算公式

android.util.TypedValue类提供了一个函数,提供所有单位与px的换算关系。

  • dpi如何计算? dpi,英文全称dot per inch,是指每英寸上的像素点个数。 计算公式:dpi=√(X²+Y²)/Z (其中 X:纵向的像素个数;Y:横向的像素个数;Z:屏幕尺寸)

  • scale是如何计算的? 定义以160dpi作为基准,当前设备的dpi/160dpi = scale值。也就是当设备的dpi为160时1dp=1px.

  • dp与px如何转换? 计算公式:pxs = dps * (dpi/160), 比如:320dpi的设备,10dp转换为px为 10 *(320/160)px = 20 px,即320dpi下10dp=20px.

  • 1dp到底等于多长呢? 假设设备的dpi=320, 是指320px对应的长度为1英寸, 那么1px=1/320 英寸。再根据前面的公式, 1dp = (320/160) px = 2px。故1dp对应的长度为2 * (1/320)英寸,即1/160英寸。 同理,假设设备的dpi =N,那么1dp = (N/160) px = (N/160) * (1/N) inch = 1/160 inch,再加上前面提到的近似,可以得出结论为1dp约等于1/160英寸


多屏适配

适配原则

  • layout中,最好采用wap_content, fill_parent,dp,可保证在屏幕上有合适的大小;
  • 像素单位使用dp, 文字单位使用sp;
  • 程序代码中不要出现具体的px;
  • 图片应多使用.9.png;
  • 使用百分比布局;

解决方案-支持各种屏幕密度

  • 解决屏幕宽度不一致问题
    思路:把任何设备的手机宽度像素均分为320份,高度像素均分为480份,使用我们写好的程序自动生成资源values-***×***文件夹,里面包含lay_x.xmllay_y.xml,分别对应宽度和高度的像素。

程序代码如下:

public class MakeXml {
    private final static String rootPath = "F:\\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;
    }

}

执行上面程序,会生成下面这些文件夹和文件:


生成的一系列文件夹

每个文件夹下的xml文件

F:\layoutroot\values-480x320\lay_x.xml

<?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="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>

F:\layoutroot\values-480x320\lay_y.xml

<?xml version="1.0" encoding="utf-8"?>
<resources><dimen name="y1">1.0px</dimen>
<dimen name="y2">2.0px</dimen>
<dimen name="y3">3.0px</dimen>
<dimen name="y4">4.0px</dimen>
<dimen name="y5">5.0px</dimen>
...省略...
<dimen name="y476">476.0px</dimen>
<dimen name="y477">477.0px</dimen>
<dimen name="y478">478.0px</dimen>
<dimen name="y479">479.0px</dimen>
<dimen name="y480">480px</dimen>
</resources>

在480×320的设备上,x2就代表2.0px,y2就代表3.0px。
在800×480的设备上,x2就代表3.0px,y2就代表3.33px。依次类推。

如何使用:

<Button
        android:background="#abd123"
        android:layout_width="@dimen/x160"
        android:layout_height="@dimen/y240"
        android:text="Button" />

这样设置,在各种屏幕宽度的设备上,此Button的宽度和高度就都占屏幕的一半。

效果如下:


各种屏幕密度

我们看到这种方式可以支持大部分屏幕宽度的设备,但是我们也看到了一些设备,如Nexus9、Nexus10上并没有显示出Button,这是因为我们生成的尺寸资源文件里没有对应分辨率的xml文件。

  • 提供备用位图
    为了让我们提供的图片符合各种屏幕密度的要求。我们需要为不同屏幕密度提供大小不同的图片。
    上篇文章中我们提到了

    在Google官方开发文档中,说明了 mdpi:hdpi:xhdpi:xxhdpi:xxxhdpi=2:3:4:6:8 的尺寸比例进行缩放。例如,一个图标的大小为48×48dp,表示在mdpi上,实际大小为48×48px,在hdpi像素密度上,实际尺寸为mdpi上的1.5倍,即72×72px,以此类推。

因此,我们要在drawabledrawable-hdpidrawable-mdpidrawable-xdpidrawable-xhdpi等文件夹下放置相同名称、符合上述比例的图片资源。系统会根据屏幕密度的不同,而选择对应的图片进行加载。

在布局文件中的简单使用:

 <Button
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:background="@drawable/ic_launcher" />

小插曲:
如果我们只提供一个图片来适配不同屏幕密度的设备的话,就要考虑放在哪个文件夹下了。

我们以Nexus5为例,如果我们把图片放在drawable-xxhdpi下,占用的内存最小(凯子哥的例子是11.65M),如果放在drawabledrawable-mdpi下,占用的内存将会非常大(凯子哥的例子是74.95M)。如果放在drawable-hdpi下占用的为35.38M(同一张图片),所以,我们要提供不同尺寸的图片来适配不同的屏幕密度,否则可能会很大程度上浪费内存。

四. 解决方案-实施自适应用户界面流程

我们如果为手机和平板设备适配了不同的布局,如我们使用的单面板和双面板,这样就导致了用户操作流程的不同。所以,我们必须做出一些必须的判断来适应用户界面流程。

  • 确定当前布局
    要确定当前设备使用的布局,可以通过布局是否显示出来做出判断。

    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;
      }
    }

    在对某些组件执行操作前先查看它们是否可用,比如菜单按钮,在api11以上用actionbar中的按钮代替了。

    Button catButton = (Button) findViewById(R.id.categorybutton);
    OnClickListener listener = /* create your listener here */;
    if (catButton != null) {
      catButton.setOnClickListener(listener);
    }
  • 根据当前布局做出响应

例如,在单面板模式下,用户点击了新闻标题,我们要打开一个新的activity来显示新闻详细信息;在双面板模式下,用户点击了新闻标题,我们要在右边面板上显示详细信息。

@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);
    }
}



文/世界是我的床(简书作者)
原文链接:http://www.jianshu.com/p/ad563d169871
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值