編輯:關於Android編程
2012年到2014年支持Android設備的種類從3997增長到18796。同時各大廠商定制的屏幕尺寸也非常多。這將非常不利於我們進行屏幕適配。這要求我們必須掌握屏幕適配技能,以便使我們的app可以適用於不同屏幕尺寸的設備上。
從上圖可以看出,主流的分辨率是前六種:1280×720、1920×1080、800×480、854×480、960×540、1184×720,不過我們有解決方案。看完這篇文章,想必你就可以解決常見的屏幕適配問題。
接下來正式進入正題。
介紹幾個在Android屏幕適配上非常重要的名詞:
屏幕尺寸是指屏幕對角線的長度。單位是英寸,1英寸=2.54厘米
屏幕分辨率是指在橫縱向上的像素點數,單位是px,1px=1像素點,一般是縱向像素橫向像素,如1280×720
屏幕像素密度是指每英寸上的像素點數,單位是dpi,即“dot per inch”的縮寫,像素密度和屏幕尺寸和屏幕分辨率有關
例如:計算Nexus5的屏幕像素密度:
屏幕尺寸:4.95inch、分辨率:1920×1080,屏幕像素密度:445
和官方給出的一樣,說明我們計算正確。
dip:Density Independent Pixels(密度無關像素)的縮寫。以160dpi為基准,1dp=1px
dp:同dip
dpi:屏幕像素密度的單位,“dot per inch”的縮寫
px:像素,物理上的絕對單位
sp:Scale-Independent Pixels的縮寫,可以根據文字大小首選項自動進行縮放。Google推薦我們使用12sp以上的大小,通常可以使用12sp,14sp,18sp,22sp,最好不要使用奇數和小數。
說明:如果A設備的參數為480×320,160dpi,B設置的參數為800×480,240dpi。我們要畫出一條和屏幕寬度一樣長的直線,如果使用px作為單位,必須在A設備上設置為320px,在B設備上設置480px。但是如果我們使用dp作為單位,由於以160dpi為基准,1dp=1px,所以A設備上設置為320dp就等於屏幕寬度(320px),在B設備上設置為320dp就等於320×(240/160)=480px,即B設備的屏幕寬度。這樣,使用dp作為單位就可以實現簡單的屏幕適配。這知識一種巧合,也有B設備的像素密度不是這樣剛剛好的,就需要我們運用別的屏幕適配技術。
用於區分不同的像素密度。
在Google官方開發文檔中,說明了 mdpi:hdpi:xhdpi:xxhdpi:xxxhdpi=2:3:4:6:8 的尺寸比例進行縮放。例如,一個圖標的大小為48×48dp,表示在mdpi上,實際大小為48×48px,在hdpi像素密度上,實際尺寸為mdpi上的1.5倍,即72×72px,以此類推。
我們可以通過以下幾種方式來支持各種屏幕尺寸:
wrap_content:根據控件的內容設置控件的尺寸
math_parent:根據父控件的尺寸大小設置控件的尺寸
weight:權重,在線性布局中可以使用weight屬性設置控件所占的比例
例如,我們要實現下圖所顯示的效果:當屏幕尺寸改變時,new reader控件兩邊的控件大小不變,new reader控件會占完剩余的空間。
實現:通過給new reader控件設置屬性:android:layout_width="wrap_content",android:layout_weight="1",兩邊兩個控件為固定大小,android:layout_weight="0"
具體布局文件如下:
小插曲:關於android:layout_weight屬性
公式:所占寬度=原來寬度+剩余空間所占百分比的寬度
一般情況,我們都是設置要進行比例分配的方向的寬度為0dp,然後再用權重進行分配。如下:
效果為:
設屏幕寬度為L,
根據公式,
button1寬度=0+L×1/(1+2)=1/3L
button2寬度=0+L×2/(1+2)=2/3L
但如果設置為match_parent:
效果為:
button1寬度=L+(L-2L)×1/3=2/3L
button2寬度=L+(L-2L)×2/3=1/3L
當然,還有其他的方式,都可以運用此公式進行計算。
在實際開發中,我們一般使用0dp的方式,而不使用其他方式。
簡單的布局一般都使用線性布局,而略微復雜點的布局,我們使用相對布局,大多數時候,我們都是使用這兩種布局的嵌套。
我們使用相對布局的原因是,相對布局能在各種尺寸的屏幕上保持控件間的相對位置。
使用尺寸限定符
當我們要在大屏幕上顯示不同的布局,就要使用large限定符。例如,在寬的屏幕左邊顯示列表右邊顯示列表項的詳細信息,在一般寬度的屏幕只顯示列表,不顯示列表項的詳細信息,我們就可以使用large限定符。
res/layout/main.xml 單面板:
res/layout-large/main.xml 雙面板:
如果這個程序運行在屏幕尺寸大於7inch的設備上,系統就會加載res/layout-large/main.xml 而不是res/layout/main.xml,在小於7inch的設備上就會加載res/layout/main.xml。
需要注意的是,這種通過large限定符分辨屏幕尺寸的方法,適用於android3.2之前。在android3.2之後,為了更精確地分辨屏幕尺寸大小,Google推出了最小寬度限定符。
使用最小寬度限定符
最小寬度限定符的使用和large基本一致,只是使用了具體的寬度限定。
res/layout/main.xml,單面板(默認)布局:
res/layout-sw600dp/main.xml,雙面板布局: Small Width 最小寬度
這種方式是不區分屏幕方向的。這種最小寬度限定符適用於android3.2之後,所以如果要適配android全部的版本,就要使用large限定符和sw600dp文件同時存在於項目res目錄下。
這就要求我們維護兩個相同功能的文件。為了避免繁瑣操作,我們就要使用布局別名。
使用布局別名
res/layout/main.xml: 單面板布局
res/layout-large/main.xml: 多面板布局
res/layout-sw600dp/main.xml: 多面板布局
由於後兩個文具文件一樣,我們可以用以下兩個文件代替上面三個布局文件:
res/layout/main.xml 單面板布局
res/layout/main_twopanes.xml 雙面板布局
然後在res下建立
res/values/layout.xml、
res/values-large/layout.xml、
res/values-sw600dp/layout.xml三個文件。
默認布局
res/values/layout.xml:
- @layout/main
Android3.2之前的平板布局
res/values-large/layout.xml:
- @layout/main_twopanes
Android3.2之後的平板布局
res/values-sw600dp/layout.xml:
- @layout/main_twopanes
這樣就有了main為別名的布局。
在activity中setContentView(R.layout.main);
這樣,程序在運行時,就會檢測手機的屏幕大小,如果是平板設備就會加載res/layout/main_twopanes.xml,如果是手機設備,就會加載res/layout/main.xml 。我們就解決了只使用一個布局文件來適配android3.2前後的所有平板設備。
使用屏幕方向限定符
如果我們要求給橫屏、豎屏顯示的布局不一樣。就可以使用屏幕方向限定符來實現。
例如,要在平板上實現橫豎屏顯示不用的布局,可以用以下方式實現。
res/values-sw600dp-land/layouts.xml:橫屏
- @layout/main_twopanes
res/values-sw600dp-port/layouts.xml:豎屏
- @layout/main
自動拉伸位圖,即android下特有的.9.png圖片格式。
當我們需要使圖片在拉伸後還能保持一定的顯示效果,比如,不能使圖片中的重要像素拉伸,不能使內容區域受到拉伸的影響,我們就可以使用.9.png圖來實現。
要使用.9.png,必須先得創建.9.png圖片,androidSDK給我們提供了的工具就包含.9.png文件的創建和修改工具。雙擊SDK安裝目錄 oolsdraw9patch.bat,就會打開下圖所示的窗口。
直接把圖片拖進去,選擇Filesave.9.png,即可保存為.9.png圖片。
不過,這和原圖沒有什麼區別,我們要制作成自動拉伸的圖片,還需要進行簡單的處理。
在左邊和上邊點下一個像素或多個像素點,是選擇了拉伸的像素,即把選擇的像素點進行拉伸。選擇的像素點越多,拉伸的程度也越大。
右邊和下邊是選擇內容區域,在右邊和下邊畫上直線,交叉的區域就是內容區域。
什麼事內容區域呢?
比如,我們給Button設置了一個.9.png作為背景,還要設置其android:text屬性,設置的text所占的位置就是內容區域。
我們必須保證內容區域包含text文本,才會正常顯示出text文本的內容。
下面是一個例子:
上圖是我們制作的.9.png,設置好了左上拉伸像素點和內容區域。
Button屬性設置:
如果我們選擇的內容區域偏差太大,可能就不會顯示出text值BUTTON。
程序代碼如下:
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 = "{1}px \n"; private final static String HTemplate = "{1}px \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("\n"); sb.append(""); 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(" "); StringBuffer sb2 = new StringBuffer(); sb2.append("\n"); sb2.append(""); 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(" "); 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; } }
執行上面程序,會生成下面這些文件夾和文件:
F:\layoutroot\values-480x320\lay_x.xml
1.0px 2.0px 3.0px 4.0px 5.0px ...省略...316.0px 317.0px 318.0px 319.0px 320px
F:\layoutroot\values-480x320\lay_y.xml
1.0px 2.0px 3.0px 4.0px 5.0px ...省略...476.0px 477.0px 478.0px 479.0px 480px
在480×320的設備上,x2就代表2.0px,y2就代表3.0px。
在800×480的設備上,x2就代表3.0px,y2就代表3.33px。依次類推。
如何使用:
這樣設置,在各種屏幕寬度的設備上,此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,以此類推。
因此,我們要在drawable、drawable-hdpi、drawable-mdpi、drawable-xdpi、drawable-xhdpi等文件夾下放置相同名稱、符合上述比例的圖片資源。系統會根據屏幕密度的不同,而選擇對應的圖片進行加載。
在布局文件中的簡單使用:
小插曲:
如果我們只提供一個圖片來適配不同屏幕密度的設備的話,就要考慮放在哪個文件夾下了。
我們以Nexus5為例,如果我們把圖片放在drawable-xxhdpi下,占用的內存最小(凱子哥的例子是11.65M),如果放在drawable或drawable-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); } }
Android快速入門 1. 搭建開發環境>解壓壓縮文件,得到:①Android SDK (類似於JDK)② Eclipse ③ADT>配置兩個pat
在很多的Android項目中都需要用戶登錄、注冊。這樣的話在開發中做好保護用戶密碼的工作就顯得尤為重要。這裡我把自己的密碼保護方法記錄下來。這是我建了一個保存密碼的文件,
在上一篇文章中我們已經完成對友盟SDK的基本集成,但是要滿足產品經理對一個APP的期望,基本集成的功能遠遠是不夠的,此時我們需要更進一步的集成友盟SDK的功能1.賬號統計
getPixels()void getPixels (int[] pixels, int offset,