适配_适配器模式 - CSDN
  • 前言Android的屏幕适配一直以来都在折磨着我们Android开发者,本文将结合: Google的官方权威适配文档 郭霖: Android官方提供的支持不同屏幕大小的全部方法 Stormzhang:Android 屏幕适配 鸿洋:Android 屏幕适配...

    前言

    Android的屏幕适配一直以来都在折磨着我们Android开发者,本文将结合:

    给你带来一种全新、全面而逻辑清晰的Android屏幕适配思路,只要你认真阅读,保证你能解决Android的屏幕适配问题!


    目录

    Android屏幕适配解决方案.png


    定义

    使得某一元素在Android不同尺寸、不同分辨率的手机上具备相同的显示效果


    相关重要概念

    屏幕尺寸

    • 含义:手机对角线的物理尺寸
    • 单位:英寸(inch),1英寸=2.54cm

    Android手机常见的尺寸有5寸、5.5寸、6寸等等

    屏幕分辨率

    • 含义:手机在横向、纵向上的像素点数总和
    1. 一般描述成屏幕的"宽x高”=AxB
    2. 含义:屏幕在横向方向(宽度)上有A个像素点,在纵向方向
      (高)有B个像素点
    3. 例子:1080x1920,即宽度方向上有1080个像素点,在高度方向上有1920个像素点
    • 单位:px(pixel),1px=1像素点

    UI设计师的设计图会以px作为统一的计量单位

    • Android手机常见的分辨率:320x480、480x800、720x1280、1080x1920

    屏幕像素密度

    • 含义:每英寸的像素点数
    • 单位:dpi(dots per ich)

    假设设备内每英寸有160个像素,那么该设备的屏幕像素密度=160dpi

    • 安卓手机对于每类手机屏幕大小都有一个相应的屏幕像素密度:
    密度类型 代表的分辨率(px) 屏幕像素密度(dpi)
    低密度(ldpi) 240x320 120
    中密度(mdpi) 320x480 160
    高密度(hdpi) 480x800 240
    超高密度(xhdpi) 720x1280 320
    超超高密度(xxhdpi) 1080x1920 480

    屏幕尺寸、分辨率、像素密度三者关系

    一部手机的分辨率是宽x高,屏幕大小是以寸为单位,那么三者的关系是:

    三者关系示意图
    数学不太差的人应该能懂…吧?

    不懂没关系,在这里举个例子
    假设一部手机的分辨率是1080x1920(px),屏幕大小是5寸,问密度是多少?
    解:请直接套公式
    解答过程

    密度无关像素

    • 含义:density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关。
    • 单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果
    1. Android开发时用dp而不是px单位设置图片大小,是Android特有的单位
    2. 场景:假如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。
    • dp与px的转换
      因为ui设计师给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么我们需要进行转换:
    密度类型 代表的分辨率(px) 屏幕密度(dpi) 换算(px/dp) 比例
    低密度(ldpi) 240x320 120 1dp=0.75px 3
    中密度(mdpi) 320x480 160 1dp=1px 4
    高密度(hdpi) 480x800 240 1dp=1.5px 6
    超高密度(xhdpi) 720x1280 320 1dp=2px 8
    超超高密度(xxhdpi) 1080x1920 480 1dp=3px 12

    在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

    独立比例像素

    • 含义:scale-independent pixel,叫sp或sip
    • 单位:sp
    1. Android开发时用此单位设置文字大小,可根据字体大小首选项进行缩放
    2. 推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清

    请把上面的概念记住,因为下面讲解都会用到!


    为什么要进行Android屏幕适配

    由于Android系统的开放性,任何用户、开发者、OEM厂商、运营商都可以对Android进行定制,于是导致:

    • Android系统碎片化:小米定制的MIUI、魅族定制的flyme、华为定制的EMUI等等

    当然都是基于Google原生系统定制的

    • Android机型屏幕尺寸碎片化:5寸、5.5寸、6寸等等
    • Android屏幕分辨率碎片化:320x480、480x800、720x1280、1080x1920

    据友盟指数显示,统计至2015年12月,支持Android的设备共有27796种

    当Android系统、屏幕尺寸、屏幕密度出现碎片化的时候,就很容易出现同一元素在不同手机上显示不同的问题。

    试想一下这么一个场景:
    为4.3寸屏幕准备的UI设计图,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白;而5.0寸的UI设计图运行到4.3寸的设备上,很可能显示不下。

    为了保证用户获得一致的用户体验效果:

    使得某一元素在Android不同尺寸、不同分辨率的手机上具备相同的显示效果

    于是,我们便需要对Android屏幕进行适配。


    屏幕适配问题的本质

    • 使得“布局”、“布局组件”、“图片资源”、“用户界面流程”匹配不同的屏幕尺寸

    使得布局、布局组件自适应屏幕尺寸;
    根据屏幕的配置来加载相应的UI布局、用户界面流程

    • 使得“图片资源”匹配不同的屏幕密度

    解决方案

    • 问题:如何进行屏幕尺寸匹配?
    • 答:

    屏幕尺寸适配解决方案.png


    “布局”匹配

    本质1:使得布局元素自适应屏幕尺寸

    • 做法
      使用相对布局(RelativeLayout),禁用绝对布局(AbsoluteLayout)

    开发中,我们使用的布局一般有:

    • 线性布局(Linearlayout)
    • 相对布局(RelativeLayout)
    • 帧布局(FrameLayout)
    • 绝对布局(AbsoluteLayout)

    由于绝对布局(AbsoluteLayout)适配性极差,所以极少使用。

    对于线性布局(Linearlayout)、相对布局(RelativeLayout)和帧布局(FrameLayout)需要根据需求进行选择,但要记住:

    • RelativeLayout
      布局的子控件之间使用相对位置的方式排列,因为RelativeLayout讲究的是相对位置,即使屏幕的大小改变,视图之前的相对位置都不会变化,与屏幕大小无关,灵活性很强
    • LinearLayout
      通过多层嵌套LinearLayout和组合使
      用"wrap_content"和"match_parent"已经可以构建出足够复杂的布局。但是LinearLayout无法准确地控制子视图之间的位置关系,只能简单的一个挨着一个地排列

    所以,对于屏幕适配来说,使用相对布局(RelativeLayout)将会是更好的解决方案

    本质2:根据屏幕的配置来加载相应的UI布局

    应用场景:需要为不同屏幕尺寸的设备设计不同的布局

    • 做法:使用限定符

    • 作用:通过配置限定符使得程序在运行时根据当前设备的配置(屏幕尺寸)自动加载合适的布局资源

    • 限定符类型:

    • 尺寸(size)限定符

    • 最小宽度(Smallest-width)限定符

    • 布局别名

    • 屏幕方向(Orientation)限定符

    尺寸(size)限定符

    • 使用场景:当一款应用显示的内容较多,希望进行以下设置:
    • 在平板电脑和电视的屏幕(>7英寸)上:实施**“双面板”**模式以同时显示更多内容
    • 在手机较小的屏幕上:使用单面板分别显示内容

    因此,我们可以使用尺寸限定符(layout-large)通过创建一个文件

    res/layout-large/main.xml
    

    来完成上述设定:

    • 让系统在屏幕尺寸>7英寸时采用适配平板的双面板布局
    • 反之(默认情况下)采用适配手机的单面板布局

    文件配置如下:

    • 适配手机的单面板(默认)布局: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>
    
    • 适配尺寸>7寸平板的双面板布局::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>
    

    请注意:

    • 两个布局名称均为main.xml,只有布局的目录名不同:第一个布局的目录名为:layout,第二个布局的目录名为:layout-large,包含了尺寸限定符(large)
    • 被定义为大屏的设备(7寸以上的平板)会自动加载包含了large限定符目录的布局,而小屏设备会加载另一个默认的布局

    但要注意的是,这种方式只适合Android 3.2版本之前。

    最小宽度(Smallest-width)限定符

    • 背景:上述提到的限定符“large”具体是指多大呢?似乎没有一个定量的指标,这便意味着可能没办法准确地根据当前设备的配置(屏幕尺寸)自动加载合适的布局资源
    • 例子:比如说large同时包含着5寸和7寸,这意味着使用“large”限定符的话我没办法实现为5寸和7寸的平板电脑分别加载不同的布局

    于是,在Android 3.2及之后版本,引入了最小宽度(Smallest-width)限定符

    定义:通过指定某个最小宽度(以 dp 为单位)来精确定位屏幕从而加载不同的UI资源

    • 使用场景

    你需要为标准 7 英寸平板电脑匹配双面板布局(其最小宽度为 600 dp),在手机(较小的屏幕上)匹配单面板布局

    解决方案:您可以使用上文中所述的单面板和双面板这两种布局,但您应使用 sw600dp 指明双面板布局仅适用于最小宽度为 600 dp 的屏幕,而不是使用 large 尺寸限定符。

    • sw xxxdp,即small width的缩写,其不区分方向,即无论是宽度还是高度,只要大于 xxxdp,就采用次此布局
    • 例子:使用了layout-sw 600dp的最小宽度限定符,即无论是宽度还是高度,只要大于600dp,就采用layout-sw 600dp目录下的布局

    代码展示:

    • 适配手机的单面板(默认)布局: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>
    
    • 适配尺寸>7寸平板的双面板布局: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 dp 的设备
      系统会自动加载 layout-sw600dp/main.xml(双面板)布局,否则系统就会选择 layout/main.xml(单面板)布局
      (这个选择过程是Android系统自动选择的)

    使用布局别名

    设想这么一个场景

    当你需要同时为Android 3.2版本前和Android 3.2版本后的手机进行屏幕尺寸适配的时候,由于尺寸限定符仅用于Android 3.2版本前,最小宽度限定符仅用于Android 3.2版本后,所以这会带来一个问题,为了很好地进行屏幕尺寸的适配,你需要同时维护layout-sw600dp和layout-large的两套main.xml平板布局,如下:

    • 适配手机的单面板(默认)布局:res/layout/main.xml
    • 适配尺寸>7寸平板的双面板布局(Android 3.2前):res/layout-large/main.xml
    • 适配尺寸>7寸平板的双面板布局(Android 3.2后)res/layout-sw600dp/main.xml

    最后的两个文件的xml内容是完全相同的,这会带来:文件名的重复从而带来一些列后期维护的问题

    于是为了要解决这种重复问题,我们引入了“布局别名”

    还是上面的例子,你可以定义以下布局:

    • 适配手机的单面板(默认)布局:res/layout/main.xml
    • 适配尺寸>7寸平板的双面板布局:res/layout/main_twopanes.xml

    然后加入以下两个文件,以便进行Android 3.2前和Android 3.2后的版本双面板布局适配:

    1. res/values-large/layout.xml(Android 3.2之前的双面板布局)
    <resources>
        <item name="main" type="layout">@layout/main_twopanes</item>
    </resources>
    
    1. res/values-sw600dp/layout.xml(Android 3.2及之后的双面板布局)
    <resources>
    <item name="main" type="layout">@layout/main_twopanes</item>
    </resources>
    

    注:

    • 最后两个文件有着相同的内容,但是它们并没有真正去定义布局,它们仅仅只是将main设置成了@layout/main_twopanes的别名
    • 由于这些文件包含 large 和 sw600dp 选择器,因此,系统会将此文件匹配到不同版本的>7寸平板上:
      a. 版本低于 3.2 的平板会匹配 large的文件
      b. 版本高于 3.2 的平板会匹配 sw600dp的文件

    这样两个layout.xml都只是引用了@layout/main_twopanes,就避免了重复定义布局文件的情况

    屏幕方向(Orientation)限定符

    • 使用场景:根据屏幕方向进行布局的调整

    取以下为例子:

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

    方法是:

    • 先定义类别:单/双面板、是否带操作栏、宽/窄

    定义在 res/layout/ 目录下的某个 XML 文件中

    • 再进行相应的匹配:屏幕尺寸(小屏、7寸、10寸)、方向(横、纵)

    使用布局别名进行匹配

    1. 在 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>
    

    2.使用布局别名进行相应的匹配
    (屏幕尺寸(小屏、7寸、10寸)、方向(横、纵))
    res/values/layouts.xml:(默认布局)

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

    可为resources设置bool,通过获取其值来动态判断目前已处在哪个适配布局

    res/values-sw600dp-land/layouts.xml
    (大屏、横向、双面板、宽-Andorid 3.2版本后)

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

    res/values-sw600dp-port/layouts.xml
    (大屏、纵向、单面板带操作栏-Andorid 3.2版本后)

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

    res/values-large-land/layouts.xml
    (大屏、横向、双面板、宽-Andorid 3.2版本前)

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

    res/values-large-port/layouts.xml
    (大屏、纵向、单面板带操作栏-Andorid 3.2版本前)

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

    这里没有完全把全部尺寸匹配类型的代码贴出来,大家可以自己去尝试把其补充完整


    “布局组件”匹配

    本质:使得布局组件自适应屏幕尺寸

    • 做法
      使用"wrap_content"、"match_parent"和"weight“来控制视图组件的宽度和高度
    • “wrap_content”
      相应视图的宽和高就会被设定成所需的最小尺寸以适应视图中的内容
    • “match_parent”(在Android API 8之前叫作"fill_parent")
      视图的宽和高延伸至充满整个父布局
    • “weight”
      1.定义:是线性布局(Linelayout)的一个独特比例分配属性
      2.作用:使用此属性设置权重,然后按照比例对界面进行空间的分配,公式计算是:控件宽度=控件设置宽度+剩余空间所占百分比宽幅
      具体可以参考这篇文章,讲解得非常详细

    通过使用"wrap_content"、"match_parent"和"weight"来替代硬编码的方式定义视图大小&位置,你的视图要么仅仅使用了需要的那边一点空间,要么就会充满所有可用的空间,即按需占据空间大小,能让你的布局元素充分适应你的屏幕尺寸


    “图片资源”匹配

    本质:使得图片资源在不同屏幕密度上显示相同的像素效果

    • 做法:使用自动拉伸位图:Nine-Patch的图片类型
      假设需要匹配不同屏幕大小,你的图片资源也必须自动适应各种屏幕尺寸

    使用场景:一个按钮的背景图片必须能够随着按钮大小的改变而改变。
    使用普通的图片将无法实现上述功能,因为运行时会均匀地拉伸或压缩你的图片

    • 解决方案:使用自动拉伸位图(nine-patch图片),后缀名是.9.png,它是一种被特殊处理过的PNG图片,设计时可以指定图片的拉伸区域和非拉伸区域;使用时,系统就会根据控件的大小自动地拉伸你想要拉伸的部分

    1.必须要使用.9.png后缀名,因为系统就是根据这个来区别nine-patch图片和普通的PNG图片的;
    2.当你需要在一个控件中使用nine-patch图片时,如

    android:background="@drawable/button"
    

    系统就会根据控件的大小自动地拉伸你想要拉伸的部分


    ”用户界面流程“匹配

    • 使用场景:我们会根据设备特点显示恰当的布局,但是这样做,会使得用户界面流程可能会有所不同。
    • 例如,如果应用处于双面板模式下,点击左侧面板上的项即可直接在右侧面板上显示相关内容;而如果该应用处于单面板模式下,点击相关的内容应该跳转到另外一个Activity进行后续的处理。

    本质:根据屏幕的配置来加载相应的用户界面流程

    • 做法
      进行用户界面流程的自适应配置:
    1. 确定当前布局
    2. 根据当前布局做出响应
    3. 重复使用其他活动中的片段
    4. 处理屏幕配置变化
    • 步骤1:确定当前布局
      由于每种布局的实施都会稍有不同,因此我们需要先确定当前向用户显示的布局。例如,我们可以先了解用户所处的是“单面板”模式还是“双面板”模式。要做到这一点,可以通过查询指定视图是否存在以及是否已显示出来。
    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;
        }
    }
    

    这段代码用于查询“报道”面板是否可用,与针对具体布局的硬编码查询相比,这段代码的灵活性要大得多。

    • 步骤2:根据当前布局做出响应
      有些操作可能会因当前的具体布局而产生不同的结果。

    例如,在新闻阅读器示例中,如果用户界面处于双面板模式下,那么点击标题列表中的标题就会在右侧面板中打开相应报道;但如果用户界面处于单面板模式下,那么上述操作就会启动一个独立活动:

    @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);
        }
    }
    
    • 步骤3:重复使用其他活动中的片段
      多屏幕设计中的重复模式是指,对于某些屏幕配置,已实施界面的一部分会用作面板;但对于其他配置,这部分就会以独立活动的形式存在。

    例如,在新闻阅读器示例中,对于较大的屏幕,新闻报道文本会显示在右侧面板中;但对于较小的屏幕,这些文本就会以独立活动的形式存在。

    在类似情况下,通常可以在多个活动中重复使用相同的 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();
    
    • 步骤3:处理屏幕配置变化
      如果我们使用独立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;
            }
            ...
    }
    

    通过上面一系列步骤,我们就完全可以建立一个可以根据用户界面配置进行自适应的应用程序App了。


    总结

    经过上面的介绍,对于屏幕尺寸大小适配问题应该是不成问题了。


    解决方案

    • 问题:如何进行屏幕密度匹配?
    • 答:

    屏幕密度匹配解决方案.png

    “布局控件”匹配

    本质:使得布局组件在不同屏幕密度上显示相同的像素效果

    • 做法1:使用密度无关像素
      由于各种屏幕的像素密度都有所不同,因此相同数量的像素在不同设备上的实际大小也有所差异,这样使用像素(px)定义布局尺寸就会产生问题。
      因此,请务必使用密度无关像素 dp 或**独立比例像素 sp **单位指定尺寸。
    • 相关概念介绍
      密度无关像素
    • 含义:density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关。
    • 单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果
    1. Android开发时用dp而不是px单位设置图片大小,是Android特有的单位
    2. 场景:假如同样都是画一条长度是屏幕一半的线,如果使用px作为计量单位,那么在480x800分辨率手机上设置应为240px;在320x480的手机上应设置为160px,二者设置就不同了;如果使用dp为单位,在这两种分辨率下,160dp都显示为屏幕一半的长度。
    • dp与px的转换
      因为ui给你的设计图是以px为单位的,Android开发则是使用dp作为单位的,那么该如何转换呢?

    | 密度类型 | 代表的分辨率(px) | 屏幕密度(dpi)|换算(px/dp) |比例|
    | ------------- |:-------------? -------------? -------------?
    | 低密度(ldpi) | 240x320 | 120 |1dp=0.75px|3|
    | 中密度(mdpi) | 320x480 | 160 |1dp=1px|4|
    | 高密度(hdpi) | 480x800 | 240|1dp=1.5px|6|
    | 超高密度(xhdpi) | 720x1280 | 320|1dp=2px|8|
    | 超超高密度(xxhdpi) | 1080x1920 | 480 |1dp=3px|12|

    在Android中,规定以160dpi(即屏幕分辨率为320x480)为基准:1dp=1px

    独立比例像素

    • 含义:scale-independent pixel,叫sp或sip
    • 单位:sp
    1. Android开发时用此单位设置文字大小,可根据用户的偏好文字大小/字体大小首选项进行缩放
    2. 推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清

    所以,为了能够进行不同屏幕像素密度的匹配,我们推荐:

    • 使用dp来代替px作为控件长度的统一度量单位
    • 使用sp作为文字的统一度量单位

    可是,请看以下一种场景:

    Nexus5的总宽度为360dp,我们现在在水平方向上放置两个按钮,一个是150dp左对齐,另外一个是200dp右对齐,那么中间留有10dp间隔;但假如同样地设置在Nexus S(屏幕宽度是320dp),会发现,两个按钮会重叠,因为320dp<200+150dp

    从上面可以看出,由于Android屏幕设备的多样性,如果使用dp来作为度量单位,并不是所有的屏幕的宽度都具备相同的dp长度

    再次明确,屏幕宽度和像素密度没有任何关联关系

    所以说,dp解决了同一数值在不同分辨率中展示相同尺寸大小的问题(即屏幕像素密度匹配问题),但却没有解决设备尺寸大小匹配的问题。(即屏幕尺寸匹配问题)

    当然,我们一开始讨论的就是屏幕尺寸匹配问题,使用match_parent、wrap_content和weight,尽可能少用dp来指定控件的具体长宽,大部分的情况我们都是可以做到适配的。

    那么该如何解决控件的屏幕尺寸和屏幕密度的适配问题呢?

    从上面可以看出:

    • 因为屏幕密度(分辨率)不一样,所以不能用固定的px
    • 因为屏幕宽度不一样,所以要小心的用dp

    因为本质上是希望使得布局组件在不同屏幕密度上显示相同的像素效果,那么,之前是绕了个弯使用dp解决这个问题,那么到底能不能直接用px解决呢?

    即根据不同屏幕密度,控件选择对应的像素值大小

    接下来介绍一种方法:百分比适配方法,步骤如下:

    1. 以某一分辨率为基准,生成所有分辨率对应像素数列表
    2. 将生成像素数列表存放在res目录下对应的values文件下
    3. 根据UI设计师给出设计图上的尺寸,找到对应像素数的单位,然后设置给控件即可

    步骤1:以某一分辨率为基准,生成所有分辨率对应像素数列表

    现在我们以320x480的分辨率为基准:

    • 将屏幕的宽度分为320份,取值为x1~x320
    • 将屏幕的高度分为480份,取值为y1~y480

    然后生成该分辨率对应像素数的列表,如下图:

    • 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="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>
    
    • 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="y480">480px</dimen>
    </resources>
    

    找到基准后,是时候把其他分辨率补全了,现今以写1080x1920的分辨率为例:

    因为基准是320x480,所以1080/320=3.375px,1920/480=4px,所以相应文件应该是

    • lay_x.xml
    <?xml version="1.0" encoding="utf-8"?>
    <resources><dimen name="x1">3.375px</dimen>
    <dimen name="x2">6.65px</dimen>
    <dimen name="x3">10.125px</dimen>
    ...
    <dimen name="x320">1080px</dimen>
    </resources>
    
    • lay_y.xml
    <?xml version="1.0" encoding="utf-8"?>
    <resources><dimen name="y1">4px</dimen>
    <dimen name="y2">8px</dimen>
    <dimen name="y3">12px</dimen>
    <dimen name="y4">16px</dimen>
    ...
    <dimen name="y480">1920px</dimen>
    </resources>
    

    用上面的方法把你需要适配的分辨率的像素列表补全吧~

    作为程序猿的我们当然不会做手写的这些蠢事!!!多谢 @鸿洋大神 提供了自动生成工具(内置了常用的分辨率),大家可以直接点击这里下载
    注:工具默认基准为400*320,当然对于特殊需求,通过命令行指定即可:

    java -jar 文件名.jar 基准宽 基准高 额外支持尺寸1的宽,额外支持尺寸1的高_额外支持尺寸2的宽,额外支持尺寸2的高:
    

    例如:需要设置的基准是800x1280,额外支持尺寸:735x1152 ;3200x4500;

    java -jar 文件名.jar 800 1280 735,1152_3200,4500
    

    步骤2:把生成的各像素数列表放到对应的资源文件

    将生成像素数列表(lay_x.xml和lay_y.xml)存放在res目录下对应的values文件(注意宽、高要对应),如下图:

    res目录下对应的values文件

    注:

    • 分辨率为480x320的资源文件应放在res/values-480x320文件夹中;同理分辨率为1920x1080的资源文件应放在res/values-1920x1080文件夹中。(其中values-480x320是分辨率限定符)
    • 必须在默认values里面也创建对应默认lay_x.xml和lay_y.xml文件,如下图
      lay_x.xml
    <?xml version="1.0" encoding="utf-8">
    <resources>
    <dimen name="x1">1.0dp</dimen>
    <dimen name="x2">2.0dp</dimen>
    ...
    </resources>
    
    • 因为对于没有生成对应分辨率文件的手机,会使用默认values文件夹,如果默认values文件夹没有(即没有对应的分辨率、没有对应dimen)就会报错,从而无法进行屏幕适配。
      (**注意对应单位改为dp,而不同于上面的px。**因为不知道机型的分辨率,所以默认分辨率文件只好默认为x1=1dp以保证尽量兼容(又回到dp老方法了),这也是这个解决方案的一个弊端

    步骤3:根据UI设计师给出某一分辨率设计图上的尺寸,找到对应像素数的单位,然后设置给控件即可

    如下图:

    <FrameLayout >
    
        <Button
            android:layout_gravity="center"
            android:gravity="center"
            android:text="@string/hello_world"
            android:layout_width="@dimen/x160"
            android:layout_height="@dimen/y160"/>
    
    </FrameLayout>
    

    总结

    使用上述的适配方式,应该能进行90%的适配了,但其缺点还是很明显:

    • 由于实际上还是使用px作为长度的度量单位,所以和google的要求使用dp作为度量单位会有所背离
    • 必须尽可能多的包含所有分辨率,因为这个是使用这个方案的基础,如果有某个分辨率缺少,将无法完成该屏幕的适配
    • 过多的分辨率像素描述xml文件会增加软件包的大小和维护的难度

    “图片资源”匹配

    本质:使得图片资源在不同屏幕密度上显示相同的像素效果

    • 做法:提供备用位图(符合屏幕尺寸的图片资源)
      由于 Android 可在各种屏幕密度的设备上运行,因此我们提供的位图资源应该始终可以满足各类密度的要求:
    密度类型 代表的分辨率(px) 系统密度(dpi)
    低密度(ldpi) 240x320 120
    中密度(mdpi) 320x480 160
    高密度(hdpi) 480x800 240
    超高密度(xhdpi) 720x1280 320
    超超高密度(xxhdpi) 1080x1920 480
    • 步骤1:根据以下尺寸范围针对各密度生成相应的图片。

    比如说,如果我们为 xhdpi 设备生成了 200x200 px尺寸的图片,就应该按照相应比例地为 hdpi、mdpi 和 ldpi 设备分别生成 150x150、100x100 和 75x75 尺寸的图片

    即一套分辨率=一套位图资源(这个当然是Ui设计师做了)

    • 步骤2:将生成的图片文件放在 res/ 下的相应子目录中(mdpi、hdpi、xhdpi、xxhdpi),系统就会根据运行您应用的设备的屏幕密度自动选择合适的图片
    • 步骤3:通过引用 @drawable/id,系统都能根据相应屏幕的 屏幕密度(dpi)自动选取合适的位图。

    注:

    • 如果是.9图或者是不需要多个分辨率的图片,放在drawable文件夹即可
    • 对应分辨率的图片要正确的放在合适的文件夹,否则会造成图片拉伸等问题。

    更好地方案解决“图片资源”适配问题

    上述方案是常见的一种方案,这固然是一种解决办法,但缺点在于:

    • 每套分辨率出一套图,为美工或者设计增加了许多工作量
    • 对Android工程文件的apk包变的很大

    那么,有没有一种方法:

    • 保证屏幕密度适配
    • 可以最小占用设计资源
    • 使得apk包不变大(只使用一套分辨率的图片资源)

    下面我们就来介绍这个方法

    • 只需选择唯一一套分辨率规格的图片资源

    方法介绍

    1. 先来理解下Android 加载资源过程
    Android SDK会根据屏幕密度自动选择对应的资源文件进行渲染加载(自动渲染)

    比如说,SDK检测到你手机的分辨率是320x480(dpi=160),会优先到drawable-mdpi文件夹下找对应的图片资源;但假设你只在xhpdi文件夹下有对应的图片资源文件(mdpi文件夹是空的),那么SDK会去xhpdi文件夹找到相应的图片资源文件,然后将原有大像素的图片自动缩放成小像素的图片,于是大像素的图片照样可以在小像素分辨率的手机上正常显示。
    具体请看http://blog.csdn.net/xiebudong/article/details/37040263

    所以理论上来说只需要提供一种分辨率规格的图片资源就可以了
    那么应该提供哪种分辨率规格呢?

    如果只提供ldpi规格的图片,对于大分辨率(xdpi、xxdpi)的手机如果把图片放大就会不清晰

    所以需要提供一套你需要支持的最大dpi分辨率规格的图片资源,这样即使用户的手机分辨率很小,这样图片缩小依然很清晰。那么这一套最大dpi分辨率规格应该是哪种呢?是现在市面手机分辨率最大可达到1080X1920的分辨率(dpi=xxdpi=480)吗?

    2. xhdpi应该是首选

    原因如下:

    • xhdpi分辨率以内的手机需求量最旺盛
      目前市面上最普遍的高端机的分辨率还多集中在720X1080范围内(xhdpi),所以目前来看xhpdi规格的图片资源成为了首选
    • 节省设计资源&工作量
      在现在的App开发中(iOS和Android版本),有些设计师为了保持App不同版本的体验交互一致,可能会以iPhone手机为基础进行设计,包括后期的切图之类的。
      设计师们一般都会用最新的iPhone6和iPhone5s(5s和5的尺寸以及分辨率都一样)来做原型设计,所有参数请看下图
    机型 分辨率(px) 屏幕尺寸(inch) 系统密度(dpi)
    iPhone 5s 640X1164 4 332
    iPhone 6 1334x750 4.7 326
    iPhone 6 Plus 1080x1920 5 400

    iPhone主流的屏幕dpi约等于320, 刚好属于xhdpi,所以选择xhdpi作为唯一一套dpi图片资源,可以让设计师不用专门为Android端切图,直接把iPhone的那一套切好的图片资源放入drawable-xhdpi文件夹里就好,这样大大减少的设计师的工作量!

    额外小tips

    • ImageView的ScaleType属性
      设置不同的ScaleType会得到不同的显示效果,一般情况下,设置为centerCrop能获得较好的适配效果。

    • 动态设置

    使用场景:有些情况下,我们需要动态的设置控件大小或者是位置,比如说popwindow的显示位置和偏移量等

    这时我们可以动态获取当前的屏幕属性,然后设置合适的数值

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

    总结

    • 本文根据现今主流Android的适配方法,以逻辑清晰的方式进行了主流Android适配方法的全面整理
    • 接下来我会介绍继续介绍Android开发中的相关知识,感兴趣的同学可以继续关注carson_ho的微信公众号

    示意图

    示意图


    请帮顶 / 评论点赞!因为你们的鼓励是我写作的最大动力!

    展开全文
  • Android 屏幕适配方案

    万次阅读 多人点赞 2015-08-01 13:20:53
    1、概述大家在Android开发时,肯定会觉得屏幕适配是个尤其痛苦的事,各种屏幕尺寸适配起来蛋疼无比。如果我们换个角度我们看下这个问题,不知道大家有没有了解过web前端开发,或者说大家对于网页都不陌生吧,其实...

    转载请标明出处:
    http://blog.csdn.net/lmj623565791/article/details/45460089
    本文出自:【张鸿洋的博客】

    1、概述

    大家在Android开发时,肯定会觉得屏幕适配是个尤其痛苦的事,各种屏幕尺寸适配起来蛋疼无比。如果我们换个角度我们看下这个问题,不知道大家有没有了解过web前端开发,或者说大家对于网页都不陌生吧,其实适配的问题在web页面的设计中理论上也存在,为什么这么说呢?电脑的显示器的分辨率、包括手机分辨率,我敢说分辨率的种类远超过Android设备的分辨率,那么有一个很奇怪的现象:

    为什么Web页面设计人员从来没有说过,尼玛适配好麻烦?

    那么,到底是什么原因,让网页的设计可以在千差万别的分辨率的分辨率中依旧能给用户一个优质的体验呢?带着这个疑惑,我问了下媳妇(前端人员),媳妇睁大眼睛问我:什么叫适配?fc,尼玛,看来的确没有这类问题。后来再我仔细的追问后,她告诉我,噢,这个尺寸呀,我都是设置为20%的~~追根到底,其实就是一个原因,网页提供了百分比计算大小。

    同样的,大家拿到UI给的设计图以后,是不是抱怨过尼玛你标识的都是px,我项目里面用dp,这什么玩意,和UI人员解释,UI妹妹也不理解。那么本例同样可以解决Android工程师和UI妹妹间的矛盾~UI给出一个固定尺寸的设计稿,然后你在编写布局的时候不用思考,无脑照抄上面标识的像素值,就能达到完美适配,理想丰不丰满~~。

    然而,Android对于不同的屏幕给出的适配方案是dp,那么dp与百分比的差距到底在哪里?

    2、dp vs 百分比

    • dp

    我们首先看下dp的定义:

    Density-independent pixel (dp)独立像素密度。标准是160dip.即1dp对应1个pixel,计算公式如:px = dp * (dpi / 160),屏幕密度越大,1dp对应 的像素点越多。
    上面的公式中有个dpi,dpi为DPI是Dots Per Inch(每英寸所打印的点数),也就是当设备的dpi为160的时候1px=1dp;

    好了,上述这些概念记不记得住没关系,只要记住一点dp是与像素无关的,在实际使用中1dp大约等于1/160inch。

    那么dp究竟解决了适配上的什么问题?可以看出1dp = 1/160inch;那么它至少能解决一个问题,就是你在布局文件写某个View的宽和高为160dp*160dp,这个View在任何分辨率的屏幕中,显示的尺寸大小是大约是一致的(可能不精确),大概是 1 inch * 1 inch。

    但是,这样并不能够解决所有的适配问题:

    • 呈现效果仍旧会有差异,仅仅是相近而已
    • 当设备的物理尺寸存在差异的时候,dp就显得无能为力了。为4.3寸屏幕准备的UI,运行在5.0寸的屏幕上,很可能在右侧和下侧存在大量的空白。而5.0寸的UI运行到4.3寸的设备上,很可能显示不下。

    以上两点,来自参考链接1

    一句话,总结下,dp能够让同一数值在不同的分辨率展示出大致相同的尺寸大小。但是当设备的尺寸差异较大的时候,就无能为力了。适配的问题还需要我们自己去做,于是我们可能会这么做:

    <?xml version="1.0" encoding="utf-8"?>  
    <resources>  
        <!-- values-hdpi 480X800 -->  
        <dimen name="imagewidth">120dip</dimen>      
    </resources>  
    
    <resources>  
        <!-- values-hdpi-1280x800 -->  
        <dimen name="imagewidth">220dip</dimen>      
    </resources>  
    
    
    <?xml version="1.0" encoding="utf-8"?>  
    <resources>  
        <!-- values-hdpi  480X320 -->  
        <dimen name="imagewidth">80dip</dimen>      
    </resources> 

    上述代码片段来自网络,也就是说,我们为了优质的用户体验,依然需要去针对不同的dpi设置,编写多套数值文件。

    可以看出,dp并没有能解决适配问题。下面看百分比。

    • 百分比
      这个概念不用说了,web中支持控件的宽度可以去参考父控件的宽度去设置百分比,最外层控件的宽度参考屏幕尺寸设置百分比,那么其实中Android设备中,只需要支持控件能够参考屏幕的百分比去计算宽高就足够了。

    比如,我现在以下几个需求:

    • 对于图片展示的Banner,为了起到该有的效果,我希望在任何手机上显示的高度为屏幕高度的1/4
    • 我的首页分上下两栏,我希望每个栏目的屏幕高度为11/24,中间间隔为1/12
    • slidingmenu的宽度为屏幕宽度的80%

    当然了这仅仅是从一个大的层面上来说,其实小范围布局,可能百分比将会更加有用。

    那么现在不支持百分比,实现上述的需求,可能需要1、代码去动态计算(很多人直接pass了,太麻烦);2、利用weight(weight必须依赖Linearlayout,而且并不能适用于任何场景)

    再比如:我的某个浮动按钮的高度和宽度希望是屏幕高度的1/12,我的某个Button的宽度希望是屏幕宽度的1/3。

    上述的所有的需求,利用dp是无法完成的,我们希望控件的尺寸可以按照下列方式编写:

       <Button
            android:text="@string/hello_world"
            android:layout_width="20%w"
            android:layout_height="10%h"/>

    利用屏幕的宽和高的比例去定义View的宽和高。

    好了,到此我们可以看到dp与百分比的区别,而百分比能够更好的解决我们的适配问题。

    • some 适配tips

    我们再来看看一些适配的tips

    1. 多用match_parent
    2. 多用weight
    3. 自定义view解决

    其实上述3点tip,归根结底还是利用百分比,match_parent相当于100%参考父控件;weight即按比例分配;自定义view无非是因为里面多数尺寸是按照百分比计算的;

    通过这些tips,我们更加的看出如果能在Android中引入百分比的机制,将能解决大多数的适配问题,下面我们就来看看如何能够让Android支持百分比的概念。

    3、百分比的引入

    1、引入

    其实我们的解决方案,就是在项目中针对你所需要适配的手机屏幕的分辨率各自简历一个文件夹。

    如下图:

    然后我们根据一个基准,为基准的意思就是:

    比如480*320的分辨率为基准

    • 宽度为320,将任何分辨率的宽度分为320份,取值为x1-x320
    • 高度为480,将任何分辨率的高度分为480份,取值为y1-y480

    例如对于800*480的宽度480:

    可以看到x1 = 480 / 基准 = 480 / 320 = 1.5 ;

    其他分辨率类似~~
    你可能会问,这么多文件,难道我们要手算,然后自己编写?不要怕,下文会说。

    那么,你可能有个疑问,这么写有什么好处呢?

    假设我现在需要在屏幕中心有个按钮,宽度和高度为我们屏幕宽度的1/2,我可以怎么编写布局文件呢?

    <FrameLayout >
    
        <Button
            android:layout_gravity="center"
            android:gravity="center"
            android:text="@string/hello_world"
            android:layout_width="@dimen/x160"
            android:layout_height="@dimen/x160"/>
    
    </FrameLayout>
    

    可以看到我们的宽度和高度定义为x160,其实就是宽度的50%;
    那么效果图:

    可以看到不论在什么分辨率的机型,我们的按钮的宽和高始终是屏幕宽度的一半。

    • 对于设计图

    假设现在的UI的设计图是按照480*320设计的,且上面的宽和高的标识都是px的值,你可以直接将px转化为x[1-320],y[1-480],这样写出的布局基本就可以全分辨率适配了。

    你可能会问:设计师设计图的分辨率不固定怎么办?下文会说~

    • 对于上文提出的几个dp做不到的

    你可以通过在引入百分比后,自己试试~~

    好了,有个最主要的问题,我们没有说,就是分辨率这么多,尼玛难道我们要自己计算,然后手写?

    2、自动生成工具

    好了,其实这样的文件夹手写也可以,按照你们需要支持的分辨率,然后编写一套,以后一直使用。

    当然了,作为程序员的我们,怎么能做这么low的工作,肯定要程序来实现:

    那么实现需要以下步骤:

    1. 分析需要的支持的分辨率

    对于主流的分辨率我已经集成到了我们的程序中,当然对于特殊的,你可以通过参数指定。关于屏幕分辨率信息,可以通过该网站查询:http://screensiz.es/phone

    1. 编写自动生成文件的程序

    代码如下

    import java.io.File;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.PrintWriter;
    
    /**
     * Created by zhy on 15/5/3.
     */
    public class GenerateValueFiles {
    
        private int baseW;
        private int baseH;
    
        private String dirStr = "./res";
    
        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";
    
        /**
         * {0}-HEIGHT
         */
        private final static String VALUE_TEMPLATE = "values-{0}x{1}";
    
        private static final String SUPPORT_DIMESION = "320,480;480,800;480,854;540,960;600,1024;720,1184;720,1196;720,1280;768,1024;800,1280;1080,1812;1080,1920;1440,2560;";
    
        private String supportStr = SUPPORT_DIMESION;
    
        public GenerateValueFiles(int baseX, int baseY, String supportStr) {
            this.baseW = baseX;
            this.baseH = baseY;
    
            if (!this.supportStr.contains(baseX + "," + baseY)) {
                this.supportStr += baseX + "," + baseY + ";";
            }
    
            this.supportStr += validateInput(supportStr);
    
            System.out.println(supportStr);
    
            File dir = new File(dirStr);
            if (!dir.exists()) {
                dir.mkdir();
    
            }
            System.out.println(dir.getAbsoluteFile());
    
        }
    
        /**
         * @param supportStr
         *            w,h_...w,h;
         * @return
         */
        private String validateInput(String supportStr) {
            StringBuffer sb = new StringBuffer();
            String[] vals = supportStr.split("_");
            int w = -1;
            int h = -1;
            String[] wh;
            for (String val : vals) {
                try {
                    if (val == null || val.trim().length() == 0)
                        continue;
    
                    wh = val.split(",");
                    w = Integer.parseInt(wh[0]);
                    h = Integer.parseInt(wh[1]);
                } catch (Exception e) {
                    System.out.println("skip invalidate params : w,h = " + val);
                    continue;
                }
                sb.append(w + "," + h + ";");
            }
    
            return sb.toString();
        }
    
        public void generate() {
            String[] vals = supportStr.split(";");
            for (String val : vals) {
                String[] wh = val.split(",");
                generateXmlFile(Integer.parseInt(wh[0]), Integer.parseInt(wh[1]));
            }
    
        }
    
        private void generateXmlFile(int w, int h) {
    
            StringBuffer sbForWidth = new StringBuffer();
            sbForWidth.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
            sbForWidth.append("<resources>");
            float cellw = w * 1.0f / baseW;
    
            System.out.println("width : " + w + "," + baseW + "," + cellw);
            for (int i = 1; i < baseW; i++) {
                sbForWidth.append(WTemplate.replace("{0}", i + "").replace("{1}",
                        change(cellw * i) + ""));
            }
            sbForWidth.append(WTemplate.replace("{0}", baseW + "").replace("{1}",
                    w + ""));
            sbForWidth.append("</resources>");
    
            StringBuffer sbForHeight = new StringBuffer();
            sbForHeight.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
            sbForHeight.append("<resources>");
            float cellh = h *1.0f/ baseH;
            System.out.println("height : "+ h + "," + baseH + "," + cellh);
            for (int i = 1; i < baseH; i++) {
                sbForHeight.append(HTemplate.replace("{0}", i + "").replace("{1}",
                        change(cellh * i) + ""));
            }
            sbForHeight.append(HTemplate.replace("{0}", baseH + "").replace("{1}",
                    h + ""));
            sbForHeight.append("</resources>");
    
            File fileDir = new File(dirStr + File.separator
                    + VALUE_TEMPLATE.replace("{0}", h + "")//
                            .replace("{1}", w + ""));
            fileDir.mkdir();
    
            File layxFile = new File(fileDir.getAbsolutePath(), "lay_x.xml");
            File layyFile = new File(fileDir.getAbsolutePath(), "lay_y.xml");
            try {
                PrintWriter pw = new PrintWriter(new FileOutputStream(layxFile));
                pw.print(sbForWidth.toString());
                pw.close();
                pw = new PrintWriter(new FileOutputStream(layyFile));
                pw.print(sbForHeight.toString());
                pw.close();
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
        }
    
        public static float change(float a) {
            int temp = (int) (a * 100);
            return temp / 100f;
        }
    
        public static void main(String[] args) {
            int baseW = 320;
            int baseH = 400;
            String addition = "";
            try {
                if (args.length >= 3) {
                    baseW = Integer.parseInt(args[0]);
                    baseH = Integer.parseInt(args[1]);
                    addition = args[2];
                } else if (args.length >= 2) {
                    baseW = Integer.parseInt(args[0]);
                    baseH = Integer.parseInt(args[1]);
                } else if (args.length >= 1) {
                    addition = args[0];
                }
            } catch (NumberFormatException e) {
    
                System.err
                        .println("right input params : java -jar xxx.jar width height w,h_w,h_..._w,h;");
                e.printStackTrace();
                System.exit(-1);
            }
    
            new GenerateValueFiles(baseW, baseH, addition).generate();
        }
    
    }

    同时我提供了jar包,默认情况下,双击即可生成,使用说明:

    下载地址见文末,内置了常用的分辨率,默认基准为480*320,当然对于特殊需求,通过命令行指定即可:

    例如:基准 1280 * 800 ,额外支持尺寸:1152 * 735;4500 * 3200;

    按照

    java -jar xx.jar width height width,height_width,height

    上述格式即可。

    到此,我们通过编写一个工具,根据某基准尺寸,生成所有需要适配分辨率的values文件,做到了编写布局文件时,可以参考屏幕的分辨率;在UI给出的设计图,可以快速的按照其标识的px单位进行编写布局。基本解决了适配的问题。

    本方案思想已经有公司投入使用,个人认为还是很不错的,如果大家有更好的方案来解决屏幕适配的问题,欢迎留言探讨或者直接贴出好文链接,大家可以将自己的经验进行分享,这样才能壮大我们的队伍~~。

    注:本方案思想来自Android Day Day Up 一群的【blue-深圳】,经其同意编写此文,上述程序也很大程度上借鉴了其分享的源码。在此标识感谢,预祝其创业成功!

    ===>后期更新

    Google已经添加了百分比支持库,详情请看:Android 百分比布局库(percent-support-lib) 解析与扩展

    ok~

    群号:463081660,欢迎入群

    下载地址

    微信公众号:hongyangAndroid
    (欢迎关注,第一时间推送博文信息)

    参考链接

    Android多屏幕适配学习笔记

    开源,原创,实用Android 屏幕适配方案分享

    展开全文
  • Android 10适配要点,作用域存储

    万次阅读 多人点赞 2020-09-03 15:07:27
    距离Android 10系统正式发布已经过去大半年左右的时间了,你的应用程序已经对它进行适配了吗?在Android 10众多的行为变更当中,有一点是非常值得引起我们重视的,那就是作用域存储。这个新功能直接颠覆了长久以来...

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每个工作日都有文章更新。

    距离Android 10系统正式发布已经过去大半年左右的时间了,你的应用程序已经对它进行适配了吗?

    在Android 10众多的行为变更当中,有一点是非常值得引起我们重视的,那就是作用域存储。这个新功能直接颠覆了长久以来我们一直惯用的外置存储空间的使用方式,因此大量App都将面临着较多代码模块的升级。

    然而,对于作用域存储这个新功能,官方的资料并不多,很多人也没有搞明白它的用法。另外它也不属于《第一行代码》现有的知识架构体系,虽然我有想过在第3版中加入这部分内容的讲解,但几经思考之后还是决定以一讲单独文章的方式来讲解这部分内容,也算是作为《第一行代码 第3版》的内容扩展吧。

    本篇文章对作用域存储进行了比较全面的解析,相信看完之后你将能够轻松地完成Android 10作用域存储的适配升级。

    理解作用域存储

    Android长久以来都支持外置存储空间这个功能,也就是我们常说的SD卡存储。这个功能使用得极其广泛,几乎所有的App都喜欢在SD卡的根目录下建立一个自己专属的目录,用来存放各类文件和数据。

    那么这么做有什么好处吗?我想了一下,大概有两点吧。第一,存储在SD卡的文件不会计入到应用程序的占用空间当中,也就是说即使你在SD卡存放了1G的文件,你的应用程序在设置中显示的占用空间仍然可能只有几十K。第二,存储在SD卡的文件,即使应用程序被卸载了,这些文件仍然会被保留下来,这有助于实现一些需要数据被永久保留的功能。

    然而,这些“好处”真的是好处吗?或 许对于开发者而言这算是好处吧,但对于用户而言,上述好处无异于一些流氓行为。因为这会将用户的SD卡空间搞得乱糟糟的,而且即使我卸载了一个完全不再使用的程序,它所产生的垃圾文件却可能会一直保留在我的手机上。

    另外,存储在SD卡上的文件属于公有文件,所有的应用程序都有权随意访问,这也对数据的安全性带来了很大的挑战。

    为了解决上述问题,Google在Android 10当中加入了作用域存储功能。

    那么到底什么是作用域存储呢?简单来讲,就是Android系统对SD卡的使用做了很大的限制。从Android 10开始,每个应用程序只能有权在自己的外置存储空间关联目录下读取和创建文件,获取该关联目录的代码是:context.getExternalFilesDir()。关联目录对应的路径大致如下:

    /storage/emulated/0/Android/data/<包名>/files
    

    将数据存放到这个目录下,你将可以完全使用之前的写法来对文件进行读写,不需要做任何变更和适配。但同时,刚才提到的那两个“好处”也就不存在了。这个目录中的文件会被计入到应用程序的占用空间当中,同时也会随着应用程序的卸载而被删除。

    那么有些朋友可能会问了,我就是需要访问其他目录该怎么办呢?比如读取手机相册中的图片,或者向手机相册中添加一张图片。为此,Android系统针对文件类型进行了分类,图片、音频、视频这三类文件将可以通过MediaStore API来进行访问,而其他类型的文件则需要使用系统的文件选择器来进行访问。

    另外,我们的应用程序向媒体库贡献的图片、音频或视频,将会自动拥有其读写权限,不需要额外申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。而如果你要读取其他应用程序向媒体库贡献的图片、音频或视频,则必须要申请READ_EXTERNAL_STORAGE权限才行。WRITE_EXTERNAL_STORAGE权限将会在未来的Android版本中废弃。

    好了,关于作用域存储的理论知识就先讲到这里,相信你已经对它有了一个基本的了解了,那么接下来我们就开始上手操作吧。

    我一定要升级吗?

    一定会有很多朋友关心这个问题,因为每当适配升级面临着需要更改大量代码的时候,大多数人的第一想法都是能不升就不升,或者能晚升就晚升。而在作用域存储这个功能上面,恭喜大家,暂时确实是可以不用升级的。

    目前Android 10系统对于作用域存储适配的要求还不是那么严格,毕竟之前传统外置存储空间的用法实在是太广泛了。如果你的项目指定的targetSdkVersion低于29,那么即使不做任何作用域存储方面的适配,你的项目也可以成功运行到Android 10手机上。

    而如果你的targetSdkVersion已经指定成了29,也没有关系,假如你还不想进行作用域存储的适配,只需要在AndroidManifest.xml中加入如下配置即可:

    <manifest ... >
      <application android:requestLegacyExternalStorage="true" ...>
        ...
      </application>
    </manifest>
    

    这段配置表示,即使在Android 10系统上,仍然允许使用之前遗留的外置存储空间的用法来运行程序,这样就不用对代码进行任何修改了。当然,这只是一种权宜之计,在未来的Android系统版本中,这段配置随时都可能会失效(Android 11中已强制启用作用域存储,这段配置在Android 11当中已不再有效)。因此,我们还是非常有必要现在就来学习一下,到底该如何对作用域存储进行适配。

    另外,本篇文章中演示的所有示例,都可以到ScopedStorageDemo这个开源库中找到其对应的源码。

    开源库地址是:https://github.com/guolindev/ScopedStorageDemo

    获取相册中的图片

    首先来学习一下如何在作用域存储当中获取手机相册里的图片。注意,虽然本篇文章中我是以图片来举例的,但是获取音频、视频的用法也是基本相同的。

    不同于过去可以直接获取到相册中图片的绝对路径,在作用域存储当中,我们只能借助MediaStore API获取到图片的Uri,示例代码如下:

    val cursor = contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null, null, "${MediaStore.MediaColumns.DATE_ADDED} desc")
    if (cursor != null) {
        while (cursor.moveToNext()) {
            val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
            val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
            println("image uri is $uri")
        }
    	cursor.close()
    }
    

    上述代码中,我们先是通过ContentResolver获取到了相册中所有图片的id,然后再借助ContentUris将id拼装成一个完整的Uri对象。一张图片的Uri格式大致如下所示:

    content://media/external/images/media/321
    

    那么有些朋友可能会问了,获取到了Uri之后,我又该怎样将这张图片显示出来呢?这就有很多种办法了,比如使用Glide来加载图片,它本身就支持传入Uri对象来作为图片路径:

    Glide.with(context).load(uri).into(imageView)
    

    而如果你没有使用Glide或其他图片加载框架,想在不借助第三方库的情况下直接将一个Uri对象解析成图片,可以使用如下代码:

    val fd = contentResolver.openFileDescriptor(uri, "r")
    if (fd != null) {
        val bitmap = BitmapFactory.decodeFileDescriptor(fd.fileDescriptor)
    	fd.close()
        imageView.setImageBitmap(bitmap)
    }
    

    上述代码中,我们调用了ContentResolver的openFileDescriptor()方法,并传入Uri对象来打开文件句柄,然后再调用BitmapFactory的decodeFileDescriptor()方法将文件句柄解析成Bitmap对象即可。

    Demo效果:

    这样我们就将获取相册中图片的方式掌握了,并且这种方式在所有的Android系统版本中都适用。

    那么接下来,我们开始学习如何将一张图片添加到相册。

    将图片添加到相册

    将一张图片添加到手机相册要相对稍微复杂一点,因为不同系统版本之间的处理方式是不太一样的。

    我们还是通过一段代码示例来直观地学习一下,代码如下所示:

    fun addBitmapToAlbum(bitmap: Bitmap, displayName: String, mimeType: String, compressFormat: Bitmap.CompressFormat) {
        val values = ContentValues()
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
        values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
        } else {
            values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
        }
        val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        if (uri != null) {
            val outputStream = contentResolver.openOutputStream(uri)
            if (outputStream != null) {
                bitmap.compress(compressFormat, 100, outputStream)
    			outputStream.close()
            }
        }
    }
    

    这段代码演示了如何将一个Bitmap对象添加到手机相册当中,我来简单解释一下。

    想要将一张图片添加到手机相册,我们需要构建一个ContentValues对象,然后向这个对象中添加三个重要的数据。一个是DISPLAY_NAME,也就是图片显示的名称,一个是MIME_TYPE,也就是图片的mime类型。还有一个是图片存储的路径,不过这个值在Android 10和之前的系统版本中的处理方式不一样。Android 10中新增了一个RELATIVE_PATH常量,表示文件存储的相对路径,可选值有DIRECTORY_DCIM、DIRECTORY_PICTURES、DIRECTORY_MOVIES、DIRECTORY_MUSIC等,分别表示相册、图片、电影、音乐等目录。而在之前的系统版本中并没有RELATIVE_PATH,所以我们要使用DATA常量(已在Android 10中废弃),并拼装出一个文件存储的绝对路径才行。

    有了ContentValues对象之后,接下来调用ContentResolver的insert()方法即可获得插入图片的Uri。但仅仅获得Uri仍然是不够的,我们还需要向该Uri所对应的图片写入数据才行。调用ContentResolver的openOutputStream()方法获得文件的输出流,然后将Bitmap对象写入到该输出流当中即可。

    以上代码即可实现将Bitmap对象存储到手机相册当中,那么有些朋友可能会问了,如果我要存储的图片并不是Bitmap对象,而是一张网络上的图片,或者是当前应用关联目录下的图片该怎么办呢?

    其实方法都是相似的,因为不管是网络上的图片还是关联目录下的图片,我们都能获取到它的输入流,只要不断读取输入流中的数据,然后写入到相册图片所对应的输出流当中就可以了,示例代码如下:

    fun writeInputStreamToAlbum(inputStream: InputStream, displayName: String, mimeType: String) {
        val values = ContentValues()
        values.put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
        values.put(MediaStore.MediaColumns.MIME_TYPE, mimeType)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
        } else {
            values.put(MediaStore.MediaColumns.DATA, "${Environment.getExternalStorageDirectory().path}/${Environment.DIRECTORY_DCIM}/$displayName")
        }
        val bis = BufferedInputStream(inputStream)
        val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        if (uri != null) {
            val outputStream = contentResolver.openOutputStream(uri)
            if (outputStream != null) {
                val bos = BufferedOutputStream(outputStream)
                val buffer = ByteArray(1024)
                var bytes = bis.read(buffer)
                while (bytes >= 0) {
                    bos.write(buffer, 0 , bytes)
                    bos.flush()
                    bytes = bis.read(buffer)
                }
                bos.close()
            }
        }
        bis.close()
    }
    

    这段代码中只是将输入流和输出流的部分重新编写了一下,其他部分和之前存储Bitmap的代码是完全一致的,相信很好理解。

    Demo效果:

    好了,这样我们就将相册图片的读取和存储问题都解决了,下面我们来探讨另外一个常见的需求,如何将文件下载到Download目录。

    下载文件到Download目录

    执行文件下载操作是一个很常见的场景,比如说下载pdf、doc文件,或者下载APK安装包等等。在过去,这些文件我们通常都会下载到Download目录,这是一个专门用于存放下载文件的目录。而从Android 10开始,我们已经不能以绝对路径的方式访问外置存储空间了,所以文件下载功能也会受到影响。

    那么该如何解决呢?主要有以下两种方式。

    第一种同时也是最简单的一种方式,就是更改文件的下载目录。将文件下载到应用程序的关联目录下,这样不用修改任何代码就可以让程序在Android 10系统上正常工作。但使用这种方式,你需要知道,下载的文件会被计入到应用程序的占用空间当中,同时如果应用程序被卸载了,该文件也会一同被删除。另外,存放在关联目录下的文件只能被当前的应用程序所访问,其他程序是没有读取权限的。

    以上几个限制条件如果不能满足你的需求,那么就只能使用第二种方式,对Android 10系统进行代码适配,仍然将文件下载到Download目录下。

    其实将文件下载到Download目录,和向相册中添加一张图片的过程是差不多的,Android 10在MediaStore中新增了一种Downloads集合,专门用于执行文件下载操作。但由于每个项目下载功能的实现都各不相同,有些项目的下载实现还十分复杂,因此怎么将以下的示例代码融合到你的项目当中是你自己需要思考的问题。

    fun downloadFile(fileUrl: String, fileName: String) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
            Toast.makeText(this, "You must use device running Android 10 or higher", Toast.LENGTH_SHORT).show()
            return
        }
        thread {
    		try {
    			val url = URL(fileUrl)
    			val connection = url.openConnection() as HttpURLConnection
    			connection.requestMethod = "GET"
    			connection.connectTimeout = 8000
    			connection.readTimeout = 8000
    			val inputStream = connection.inputStream
    			val bis = BufferedInputStream(inputStream)
    			val values = ContentValues()
    			values.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
    			values.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
    			val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
    			if (uri != null) {
    				val outputStream = contentResolver.openOutputStream(uri)
    				if (outputStream != null) {
    					val bos = BufferedOutputStream(outputStream)
    					val buffer = ByteArray(1024)
    					var bytes = bis.read(buffer)
    					while (bytes >= 0) {
    						bos.write(buffer, 0 , bytes)
    						bos.flush()
    						bytes = bis.read(buffer)
    					}
    					bos.close()
    				}
    			}
    			bis.close()
    		} catch(e: Exception) {
    			e.printStackTrace()
    		}
        }
    }
    

    这段代码总体来讲还是比较好理解的,主要就是添加了一些Http请求的代码,并将MediaStore.Images.Media改成了MediaStore.Downloads,其他部分几乎是没有变化的,我就不再多加解释了。

    注意,上述代码只能在Android 10或更高的系统版本上运行,因为MediaStore.Downloads是Android 10中新增的API。至于Android 9及以下的系统版本,请你仍然使用之前的代码来进行文件下载。

    Demo效果:


    使用文件选择器

    如果我们要读取SD卡上非图片、音频、视频类的文件,比如说打开一个PDF文件,这个时候就不能再使用MediaStore API了,而是要使用文件选择器。

    但是,我们不能再像之前的写法那样,自己写一个文件浏览器,然后从中选取文件,而是必须要使用手机系统中内置的文件选择器。示例代码如下:

    const val PICK_FILE = 1
    
    private fun pickFile() {
        val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
        intent.addCategory(Intent.CATEGORY_OPENABLE)
        intent.type = "*/*"
        startActivityForResult(intent, PICK_FILE)
    }
    
    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        when (requestCode) {
            PICK_FILE -> {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    val uri = data.data
                    if (uri != null) {
                        val inputStream = contentResolver.openInputStream(uri)
    					// 执行文件读取操作
                    }
                }
            }
        }
    }
    

    这里在pickFile()方法当中通过Intent去启动系统的文件选择器,注意Intent的action和category都是固定不变的。而type属性可以用于对文件类型进行过滤,比如指定成image/就可以只显示图片类型的文件,这里写成/*表示显示所有类型的文件。注意type属性必须要指定,否则会产生崩溃。

    然后在onActivityResult()方法当中,我们就可以获取到用户选中文件的Uri,之后通过ContentResolver打开文件输入流来进行读取就可以了。

    Demo效果:


    第三方SDK不支持作用域存储怎么办?

    阅读完了本篇文章之后,相信你对Android 10作用域存储的用法和适配基本上都已经掌握了。然而我们在实际的开发工作当中还可能会面临一个非常头疼的问题,就是我自己的代码当然可以进行适配,但是项目中使用的第三方SDK还不支持作用域存储该怎么办呢?

    这个情况确实是存在的,比如我之前使用的七牛云SDK,它的文件上传功能要求你传入的就是一个文件的绝对路径,而不支持传入Uri对象,大家应该也会碰到类似的问题。

    由于我们是没有权限修改第三方SDK的,因此最简单直接的办法就是等待第三方SDK的提供者对这部分功能进行更新,在那之前我们先不要将targetSdkVersion指定到29,或者先在AndroidManifest文件中配置一下requestLegacyExternalStorage属性。

    然而如果你不想使用这种权宜之计,其实还有一个非常好的办法来解决此问题,就是我们自己编写一个文件复制功能,将Uri对象所对应的文件复制到应用程序的关联目录下,然后再将关联目录下这个文件的绝对路径传递给第三方SDK,这样就可以完美进行适配了。这个功能的示例代码如下:

    fun copyUriToExternalFilesDir(uri: Uri, fileName: String) {
        val inputStream = contentResolver.openInputStream(uri)
        val tempDir = getExternalFilesDir("temp")
        if (inputStream != null && tempDir != null) {
            val file = File("$tempDir/$fileName")
            val fos = FileOutputStream(file)
            val bis = BufferedInputStream(inputStream)
            val bos = BufferedOutputStream(fos)
            val byteArray = ByteArray(1024)
            var bytes = bis.read(byteArray)
            while (bytes > 0) {
                bos.write(byteArray, 0, bytes)
                bos.flush()
                bytes = bis.read(byteArray)
            }
            bos.close()
            fos.close()
        }
    }
    

    好的,关于Android 10作用域存储的重要知识点就讲到这里,相信你已经可以完全掌握了。下篇文章中我们会继续学习Android 10适配,讲一讲深色主题的功能,详见链见: Android 10适配要点,深色主题

    注:本篇文章中演示的所有示例,都可以到ScopedStorageDemo这个开源库中找到其对应的源码。

    开源库地址是:https://github.com/guolindev/ScopedStorageDemo


    本篇文章是《第一行代码 第3版》的配套扩展文章,目前《第一行代码 第3版》已经出版,Kotlin、Jetpack、MVVM,你所关心的知识点都在这里,详情点击这里查看

    京东购买地址

    当当购买地址

    天猫购买地址


    关注我的技术公众号,每天都有优质技术文章推送。

    微信扫一扫下方二维码即可关注:

    展开全文
  • 随着智能手机的普及,移动端web开发需求越来越多,在做移动端web开发时首先要解决的就是适配问题,本文将介绍两种常用适配方案。 视口 在做移动端适配时,首先需要了解视口的概念,以及如何用viewport元标签设置视口...

    随着智能手机的普及,移动端web开发需求越来越多,在做移动端web开发时首先要解决的就是适配问题,本文将介绍两种常用适配方案。

    视口

    在做移动端适配时,首先需要了解视口的概念,以及如何用viewport元标签设置视口。

    1. 布局视口(layout viewport)

    • CSS布局,是以布局视口做为参照系来计算的。
    • 一般手机浏览器的默认布局视口宽度为980px(黑莓和IE的宽度为1024px)

    2. 视觉视口(visual viewport)

    • 视觉视口是页面当前显示在屏幕上的部分

    3. 理想视口(ideal viewport)

    • 理想视口的大小取决于设备独立像素的大小

    meta

    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    复制代码
    • width=device-width 让布局视口的宽度等于设备独立像素的宽度,也就是布局视口的大小等于理想视口的大小
    • initial-scale=1 视觉视口的缩放比为1,视觉视口的大小等于理想视口的大小

    概念

    1. 物理像素(physical pixel)

    物理像素又被称为设备像素,是显示设备的最小物理单元

    2. 设备独立像素(density-independent pixel)

    设备独立像素也称为密度无关像素,一个可以由程序使用并控制的虚拟像素

    3. CSS像素

    CSS像素也称为设备无关的像素(device-independent pixel),简称DIPs。用于网页的布局。

    4. 设备像素比(device pixel ratio)

    设备像素比 = 物理像素 / 设备独立像素

    示例:iPhone6

    1. 物理像素:750pt * 1334pt
    2. 设备独立像素: 375pt * 667pt
    3. 设备像素比dpr:2

    适配方案

    1. rem适配

    rem的概念:

    • 根元素的字体大小计算,也就是html的font-size
    • 例:html的font-size为75px, 1rem的大小就是75px

    适配过程:

    1. 增加viewport的meta标签
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    复制代码
    1. js获取页面宽度,计算rem,并设置html的font-size
    var width = document.documentElement.clientWidth;
    var rem = width / 10;
    document.documentElement.style.fontSize = rem + 'px';
    复制代码
    1. 编写样式,根据设计稿计算rem值
    • 例:750px*1334px设计稿中,一块区域宽度为100px
    • CSS样式:
    .demo{
        width: 1.33333333rem; /* 1/(750/10)*100 */
    }
    复制代码
    • 在计算rem的时候,除数不一定是10,也可以是其他的数。
    • 例:var rem = width / 16, 在计算css样式时, width: 2.133333333rem /* 1/(750/16)*100 */
    • 在计算rem的时候,除数不能太大, 由于有些浏览器限制了最小字体,计算时会出现偏差。
    • 例如:设计稿为375px宽,有一块100px宽的区域,手机页面宽度也为375px,那么 手机上显示那块区域的宽度也应该为100px。当除数为50时,rem = 7.5px, 计算样式width = 1/(375/50)100 = 13.33333333rem,由于chrome强制字体最小值为12px,低于12px按12px处理,显示时认为是12px13.33333333=160px,这时显示达不到预期效果。

    2. vw适配

    viewport单位:

    • vw:是Viewport's width的简写,1vw等于window.innerWidth的1%
    • vh:和vw类似,是Viewport's height的简写,1vh等于window.innerHeihgt的1%
    • vmin:vmin的值是当前vw和vh中较小的值
    • vmax:vmax的值是当前vw和vh中较大的值

    适配过程:

    1. 增加viewport的meta标签
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
    复制代码
    1. 编写样式,根据设计稿计算vw值
    • 例:750px*1334px设计稿中,一块区域宽度为100px
    • CSS样式
    .demo{
        width: 13.33333333; /* (1/(750*1%)*100) */
    }
    复制代码

    vw在某些设备上有兼容问题

    lib-flexible 适配方案

    1. 给html设置data-dpr
    • 苹果设备设置为window.devicePixelRatio获取到的设备像素比
    • 其他设备设置为1
    <!-- iphone6/7/8 -->
    <html data-dpr="2">
    ...
    </html>
    <!-- iphone6/7/8 Plus -->
    <html data-dpr="3">
    ...
    </html>
    <!-- 其他设备 -->
    <html data-dpr="1">
    ...
    </html>
    复制代码
    2. 设置viewport
    • 计算缩放比:var scale = 1 / dpr
    • 文档中插入viewport meta元素
    <!-- dpr = 1-->
    <meta name="viewport" content="initial-scale=scale,maximum-scale=scale,minimum-scale=scale,user-scalable=no">
    <!-- dpr = 2 (iphone6/7/8) -->
    <meta name="viewport" content="initial-scale=0.5,maximum-scale=0.5,minimum-scale=0.5,user-scalable=no">
    <!-- dpr = 3 (iphone6/7/8 Plus) -->
    <meta name="viewport" content="initial-scale=0.3333333333,maximum-scale=0.3333333333,minimum-scale=0.3333333333,user-scalable=no">
    复制代码
    3. 计算html的font-size
    • 获取页面宽度
    var width = document.documentElement.getBoundingClientRect().width
    复制代码
    • 计算rem
    var rem =  width / 10
    复制代码
    • 设置html的字体大小
    document.documentElement.style.fontSize = rem + 'px';
    复制代码

    示例:

    设计稿

    • 设计稿为750px * 1334px
    • 有一块区域个宽度为750px,高度为120px,有背景图,上面文字上下垂直方向都居中,字体大小为28px

    样式

    [data-dpr="1"] .demo{
    	font-size: 14px;
    }
    [data-dpr="2"] .demo{
    	font-size: 28px;
    }
    [data-dpr="3"] .demo{
    	font-size: 42px;
    }
    .demo{
    	width: 10rem; /*750/(750/10)*/
    	height: 1.6rem; /*120/(750/10)*/
    	line-height: 1.6rem;
    	text-align: center;
    	vertical-align: middle;
    	background: #000 url('./img/bg@2x.png') no-repeat;
    	background-size: cover;
    	color: #fff;
    }
    [data-dpr="3"] .demo{
    	background-image: url('./img/bg@3x.png');
    }
    复制代码
    • 实际开发中可使用Sass、LESS以及PostCSS自动转换rem
    • PostCSS插件:postcss-px2rem-dpr

    lib-flexible源码

    ;(function(win, lib) {
        var doc = win.document;
        var docEl = doc.documentElement;
        var metaEl = doc.querySelector('meta[name="viewport"]');
        var flexibleEl = doc.querySelector('meta[name="flexible"]');
        var dpr = 0;
        var scale = 0;
        var tid;
        var flexible = lib.flexible || (lib.flexible = {});
    
        if (metaEl) {
            console.warn('将根据已有的meta标签来设置缩放比例');
            var match = metaEl.getAttribute('content').match(/initial\-scale=([\d\.]+)/);
            if (match) {
                scale = parseFloat(match[1]);
                dpr = parseInt(1 / scale);
            }
        } else if (flexibleEl) {
            var content = flexibleEl.getAttribute('content');
            if (content) {
                var initialDpr = content.match(/initial\-dpr=([\d\.]+)/);
                var maximumDpr = content.match(/maximum\-dpr=([\d\.]+)/);
                if (initialDpr) {
                    dpr = parseFloat(initialDpr[1]);
                    scale = parseFloat((1 / dpr).toFixed(2));
                }
                if (maximumDpr) {
                    dpr = parseFloat(maximumDpr[1]);
                    scale = parseFloat((1 / dpr).toFixed(2));
                }
            }
        }
    
        if (!dpr && !scale) {
            var isAndroid = win.navigator.appVersion.match(/android/gi);
            var isIPhone = win.navigator.appVersion.match(/iphone/gi);
            var devicePixelRatio = win.devicePixelRatio;
            if (isIPhone) {
                // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
                if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {
                    dpr = 3;
                } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
                    dpr = 2;
                } else {
                    dpr = 1;
                }
            } else {
                // 其他设备下,仍旧使用1倍的方案
                dpr = 1;
            }
            scale = 1 / dpr;
        }
    
        docEl.setAttribute('data-dpr', dpr);
        if (!metaEl) {
            metaEl = doc.createElement('meta');
            metaEl.setAttribute('name', 'viewport');
            metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
            if (docEl.firstElementChild) {
                docEl.firstElementChild.appendChild(metaEl);
            } else {
                var wrap = doc.createElement('div');
                wrap.appendChild(metaEl);
                doc.write(wrap.innerHTML);
            }
        }
    
        function refreshRem(){
            var width = docEl.getBoundingClientRect().width;
            if (width / dpr > 540) {
                width = 540 * dpr;
            }
            var rem = width / 10;
            docEl.style.fontSize = rem + 'px';
            flexible.rem = win.rem = rem;
        }
    
        win.addEventListener('resize', function() {
            clearTimeout(tid);
            tid = setTimeout(refreshRem, 300);
        }, false);
        win.addEventListener('pageshow', function(e) {
            if (e.persisted) {
                clearTimeout(tid);
                tid = setTimeout(refreshRem, 300);
            }
        }, false);
    
        if (doc.readyState === 'complete') {
            doc.body.style.fontSize = 12 * dpr + 'px';
        } else {
            doc.addEventListener('DOMContentLoaded', function(e) {
                doc.body.style.fontSize = 12 * dpr + 'px';
            }, false);
        }
    
    
        refreshRem();
    
        flexible.dpr = win.dpr = dpr;
        flexible.refreshRem = refreshRem;
        flexible.rem2px = function(d) {
            var val = parseFloat(d) * this.rem;
            if (typeof d === 'string' && d.match(/rem$/)) {
                val += 'px';
            }
            return val;
        }
        flexible.px2rem = function(d) {
            var val = parseFloat(d) / this.rem;
            if (typeof d === 'string' && d.match(/px$/)) {
                val += 'rem';
            }
            return val;
        }
    
    })(window, window['lib'] || (window['lib'] = {}));
    复制代码

    转载于:https://juejin.im/post/5b0a87df6fb9a07abb24128e

    展开全文
  • 大屏页面能够在大屏和电脑自适应

    千次阅读 2020-08-17 13:48:52
    5760*2160是大屏比例,原型设计按照这个开发,如果通过zoom方式修改,只能针对大屏适应,下面的代码能够让大屏页面即在大屏适应展示,也能够在电脑适应展示 //监听浏览器变化 window.onresize = function() { ...
  • web前端页面适配方法

    万次阅读 2018-10-23 09:09:21
    流式布局:就是百分比布局,非固定像素,内容向两侧填充,...适配要求: 1. 网页宽度必须和浏览器保持一致 2. 默认显示的缩放比例和PC端保持(缩放比例1.0) 3. 不允许用户自行缩放网页 满足这些要求达到了适...
  • 设计模式 | 适配器模式及典型应用

    万次阅读 多人点赞 2019-07-12 19:14:33
    适配器模式 适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器...根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适...
  • H5移动端适配总结

    千次阅读 2019-06-17 16:28:07
    近段时间以来在做微信公众号的H5开发,然而安装UI设计人员给出的尺寸在不同的移动端设备上的表现并不一致,并且在界面适配方面本以为只要在开发过程中尽量使用百分比和flex布局等就可以实现,然而在不同分辨率的手机...
  • 前端解决移动端适配的五种方法

    千次阅读 2020-05-11 13:42:50
    移动端适配的五种方法 所谓移动端适配,就是WebApp在不同尺寸的屏幕上等比显示 第一种方法:viewport适配 原理:通过设置 initial-scale , 将所有设备布局视口的宽度调整为设计图的宽度. //获取meta节点 var ...
  • H5页面适配 iPhoneX,就是这么简单

    万次阅读 2018-04-19 20:28:38
    iPhoneX 取消了物理按键,改成底部小黑条,这一改动导致网页出现了比较尴尬的屏幕适配问题。对于网页而言,顶部(刘海部位)的适配问题浏览器已经做了处理,所以我们只需要关注底部与小黑条的适配问题即可(即常见...
  • 移动端页面适配方案及原理

    万次阅读 2018-09-05 21:26:00
    本人未雨绸缪,学习一波移动端适配方案。 0. 移动端 vs PC端区别 1. 页面的三个视窗和缩放 1.1 layout viewport和visual viewport: 1.2 ideal viewport: 1.3 页面的缩放 2. 移动端适配 2.1 使用meta标签控制...
  • Android AutoLayout全新的适配方式 堪称适配终结者

    万次阅读 多人点赞 2017-06-26 15:13:31
    转载请标明出处: ... 本文出自:【张鸿洋的博客】 一、概述相信Android的开发者对于设配问题都比较苦恼,Google官方虽然给出了一系列的建议,但是...个人也比较关注适配的问题,之前也发了几篇关于适配的文章,大致有:
  • Android屏幕适配全攻略(最权威的官方适配指导)

    万次阅读 多人点赞 2015-10-29 14:37:36
    Android的屏幕适配一直以来都在折磨着我们这些开发者,本篇文章以Google的官方文档为基础,全面而深入的讲解了Android屏幕适配的原因、重要概念、解决方案及最佳实践,我相信如果你能认真的学习本文,对于Android的...
  • 关于Android 10.0适配,看这篇就够了

    万次阅读 多人点赞 2019-11-19 13:00:52
    本文将从三个角度介绍Android Q的部分适配问题,也是大家开发适配过程中大概率会遇到的问题: Q 行为变更:所有应用 (不管targetSdk是多少,对所有跑在Q设备上的应用均有影响) Q 行为变更:以 Android Q 为目标...
  • 前端iPhone刘海屏适配

    万次阅读 2020-05-22 15:12:32
    对于iPhone系列出的越来越多,如果只是使用@media来做适配的话,老代码想要适配新机型还是有一定局限性的。 今天去搜了搜相关的解决方法,那么就来总结一下。 安全区 早期苹果对于 iPhone X 的设计布局意见如下: ...
  • flutter 屏幕适配 字体大小适配

    万次阅读 热门讨论 2020-07-25 12:14:35
    比如我们的设计稿一个View的大小是300px,如果直接写300px,可能在当前设备显示正常,但到了其他设备可能就会偏小或者偏大,这就需要我们对屏幕进行适配。 安卓原生的话有自己的适配规则,可以根据不同的尺寸建立...
  • Android 悬浮窗权限各机型各系统适配大全

    万次阅读 多人点赞 2017-05-02 09:54:27
    这篇博客主要介绍的是 Android 主流各种机型和各种版本的悬浮窗权限适配,但是由于碎片化的问题,所以在适配方面也无法做到完全的主流机型适配,这个需要大家的一起努力,这个博客的名字永远都是一个将来时,感兴趣...
  • Android 屏幕适配之dimens适配

    万次阅读 热门讨论 2018-03-01 09:39:20
    在过去多个项目中一直使用dimens适配,这种适配方式的优点很明显,不用管什么dp还是dpi这些东西,只需要以一种屏幕分辨率为基准(例如1280x720,相当于把屏幕宽分成720份,高分成1280份),生成对应屏幕分辨率的的dimens文件...
  • Android屏幕适配dp、px两套解决办法

    万次阅读 多人点赞 2018-08-25 21:05:11
    “又是屏幕适配,这类文章网上不是很多了吗?” 我也很遗憾,确实又是老问题。但本文重点对网上的各种方案做一个简短的总结,和具体使用方法。 若想了解具体android设备适配的前世因果,请阅读hongyang文章:...
  • iPhone屏幕尺寸、分辨率及适配

    万次阅读 多人点赞 2017-07-29 12:08:57
    从初代iPhone3GS到现如今的...如何适配不同的屏幕尺寸,使UI更加协调美观,这给iPhone/iOS应用开发者带来了挑战。 本文结合个人在iOS UI开发和适配方面的粗浅经验,对常用屏幕适配相关因素做个梳理盘点,以备日后查阅。
1 2 3 4 5 ... 20
收藏数 237,920
精华内容 95,168
关键字:

适配