編輯:關於Android編程
Android的源碼公開策略豐富了手持設備的多樣性,但隨之而來的卻是較為嚴重的”碎片化”——版本繁多、尺寸多樣、功能定制。在Android項目開發中,軟件工程師都會面臨一個問題:如何適配多不同分辨率的設備?
許多人采用的是這樣的方式:利用不同的dimens和drawable資源適配不同分辨率的設備。這麼做當然沒錯,可是它也同時帶來一些弊端
在調試UI時挨個修改多個dimen文件中的每個值。嗯哼,毫不避諱的說:以上這些坑我都掉進去過,有的坑還有點深,快到我脖子了。
當我最後一次掉在坑裡的時候,我就下定決心,我要想個辦法:一套圖片,一套布局,一個dimen完成多分辨率的適配!
哇哈,如果你也有這個想法,那就上車吧!
在此以華為P7為例,解釋inch、px、pt、dpi、dip、densityDpi、TypedValue、sp等等Android中常見的度量單位
inch
inch即為英寸,它表示設備的物理屏幕的對角線長度。
比如該例中P7的屏幕尺寸為5英寸,表示的就是手機的右上角與左下角之間的距離,其中1 inch = 2.54 cm
px
pixel簡稱為px,它表示屏幕的像素,也就是大家常說的屏幕分辨率。
比如在該例中P7的分辨率為1920*1080,它表示屏幕的X方向上有1080個像素,Y方向上有1920個像素。
pt
pt類似於px,但常用於字體的單位,不再贅述
dpi和densityDpi
dot per inch簡稱為dpi,它表示每英寸上的像素點個數,所以它也常為屏幕密度。
在Android中使用DisplayMetrics中的densityDpi字段表示該值,並且不少文檔中常用dpi來簡化或者指代densityDpi。
在手機屏幕一定的情況下,如果分辨率越高那麼該值則越大,這就意味著畫面越清晰、細膩和逼真。
在此,仍然以華為P7為例,計算其dpi值。先利用勾股定理得其對角線的像素值為2202.91,再除以對角線的大小5,即2202.91/5=440.582;此處計算出的440.582便是該設備的屏幕密度。
Android中依據densityDpi的不同將設備分成了多個顯示級別:
ldpi、mdpi、hdpi、xhdpi、xxhdpi
這些顯示級別分別表示一定范圍的dpi,比如160dpi—240dpi都稱為hdpi,更多詳情請參見下圖。
其實,在Android的源碼中也定義了這些常量,比如:
public static final int DENSITY_LOW = 120;
public static final int DENSITY_MEDIUM = 160;
public static final int DENSITY_XXHIGH = 480;
嗯哼,在了解了這些之後,現在我們再通過代碼來獲取設備的dpi值
private void getDisplayInfo(){ Resources resources=getResources(); DisplayMetrics displayMetrics = resources.getDisplayMetrics(); float density = displayMetrics.density; int densityDpi = displayMetrics.densityDpi; System.out.println("----> density=" + density); System.out.println("----> densityDpi=" + densityDpi); }
輸出結果:
—-> density=3.0
—-> densityDpi=480
呃,獲取到的densityDpi是480和我們計算出來的屏幕實際密度值440.582不一樣。這是為什麼呢?
在每部手機出廠時都會為該手機設置屏幕密度,若其屏幕的實際密度是440dpi那麼就會將其屏幕密度設置為與之接近的480dpi;如果實際密度為325dpi那麼就會將其屏幕密度設置為與之接近的320dpi。這也就是說常見的屏幕密度是與每個顯示級別的最大值相對應的,比如:120、160、240、320、480、640等。順便說一下,看到代碼中的density麼?嗯哼,其實它就是一個倍數關系罷了,它表示當前設備的densityDpi和160的比值,例如此處480/160=3。為啥是除以160而不是其他數值呢?甭急,馬上就會講到了。
話說,林子大了什麼鳥都有,有的手機不一定會選擇120、160、240、320、480、640中的值作為屏幕密度,而是選擇實際的dpi作為屏幕密度。比如為了發燒而生的小咪手機,它的某些機型的densityDpi就是個非常規的值。
其實,關於這一點,我們從Android源碼對於densityDpi的注釋也可以看到一些端倪:
The screen density expressed as dots-per-inch.
May be either DENSITY_LOW,DENSITY_MEDIUM or DENSITY_HIGH
請注意這裡的措辭”May be”,它也沒有說一定非要是DENSITY_LOW、DENSITY_MEDIUM、 DENSITY_HIGH這些系統常量。
好吧,這可能就是Android”碎片化”的一個佐證吧。
dp
density-independent pixel簡稱為dip或者dp,它表示與密度無關的像素。
如果使用dp作為長度單位,那麼該長度在不同密度的屏幕中顯示的比例將保持一致。
既然dp與密度無關,那麼它與px又有什麼關系呢?
在剛提到的Android的多個顯示級別中有一個mdpi,它被稱為基准密度
正如官方文檔所言:
The density-independent pixel is equivalent to one physical pixel on a 160 dpi screen, which is the baseline density assumed by the system for a “medium” density screen.
當dpi=160時1px=1dp,也就是說所有dp和px的轉換都是基於mdpi而言的。
比如當dpi=320(即xhdpi)時1dp=2px;當dpi=480(即xxhdpi)時1dp=3px,該過程的換算公式為:
dp * (dpi / 160)
完整的對應關系,請參照下圖。
sp
scale-independent pixel簡稱為sp,它類似於dp,但主要用於表示字體的大小,不再贅述
TypedValue
剛才提到,依據densityDpi的不同將設備分成了多個顯示級別:ldpi、mdpi、hdpi、xhdpi、xxhdpi。看到這句話時想必很多人都覺得這個玩意太眼熟了,在res下不是有drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxdpi文件夾麼?是的,但是它們有什麼聯系麼?
之前也說了:Android設備千差萬別,不同設備的屏幕密度(densityDpi)自然也就各不相同,有的屬於mdpi,某些又屬於xhdpi,或者xxhdpi等等其他顯示級別。設計師為了讓同一個APP在各種手機上都獲得較好的顯示效果就會針對densityDpi的不同而單獨提供一套UI圖。
比如,客戶要求APP適配顯示級別為:ldpi、mdpi、hdpi、xhdpi、xxhdpi的設備,那麼UI設計師就需要5套尺寸不一的UI圖分別放入res下的drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi文件夾裡。當手機設備的顯示級別為hdpi時,此時APP會去加載drawable-hdpi中對應圖片;同理如果手機的顯示級別為xxhdpi那麼APP就會去自動加載drawable-xxhdpi中的資源圖片。
關於此處的這種對應關系,我們再來看一段代碼:
/** * 原創作者: * 谷哥的小弟 * * 博客地址: * http://blog.csdn.net/lfdfhl */ private void getDrawableFolderDensity(){ TypedValue typedValue = new TypedValue(); Resources resources=mContext.getResources(); int id = getResources().getIdentifier(imageName, "drawable" , packageName); resources.openRawResource(id, typedValue); int density=typedValue.density; System.out.println("----> density="+density); }
在此,我們可以發現:
如果將圖片放入drawable-ldpi中那麼density值為120
如果將圖片放入drawable-mdpi那麼density的值為160
類似地操作總結如下圖:
嗯哼,看到這是不是就將densityDpi和TypedValue中的density理解性地結合在一起了呢?說白了,設備會去res下找尋與之適應的資源圖片,在這個找尋的過程中判斷”是否合適”的方式就是將自身的densityDpi與res文件夾的TypedValue.density字段相比較。
TypedValue中除了剛說的density字段外,還有一個挺重要的方法applyDimension( ),源碼如下:
public static float applyDimension(int unit, float value,DisplayMetrics metrics) { switch (unit) { case COMPLEX_UNIT_PX: return value; case COMPLEX_UNIT_DIP: return value * metrics.density; case COMPLEX_UNIT_SP: return value * metrics.scaledDensity; case COMPLEX_UNIT_PT: return value * metrics.xdpi * (1.0f/72); case COMPLEX_UNIT_IN: return value * metrics.xdpi; case COMPLEX_UNIT_MM: return value * metrics.xdpi * (1.0f/25.4f); } return 0; }
該方法的作用是把Android系統中的非標准度量尺寸(比如dip、sp、pt等)轉變為標准度量尺寸px。在這段代碼裡,同樣可以見到一個density;但是請注意它是DisplayMetrics中的字段而不是TypedValue的,請注意區分。
這得從一次掉坑的經歷說起。
有天下午,都快下班了,測試妹子跑到我工位前,急匆匆地說:圖片失真了。哎,又不是失身,急啥嘛。我慢條斯理地瞅瞅了代碼:代碼沒錯呀,以前也都是這些的呀。到底是哪裡出了幺蛾子呢?經過一番排查,發現是圖片放錯了地方:本來是該放到drawable-xxhdpi中的但是小手一抖錯放到了drawable-xhdpi中導致了圖片放大失真。
嗯哼,這個坑我們可能自己踩過,或者說這個現象我們略知一二,但是導致這個現象的原因是什麼呢?它的背後隱藏著什麼呢?
來吧,一起瞅瞅。
在此,准備了一張圖,該圖就是我的CSDN博客頭像
圖片的寬為144,高為180。
然後在res文件夾下建立drawable-ldpi、drawable-mdpi、drawable-hdpi、drawable-xhdpi、drawable-xxhdpi、drawable-xxxdpi文件夾,並且將該圖片放入drawable-xxhdpi中
再利用ImageView顯示該圖片,代碼如下:
運行之後,看一下效果
最後,在Java代碼中獲取圖片的寬高及其所占內存的大小,代碼如下:
private void getImageInfo() { mImageView.post(new Runnable() { @Override public void run() { BitmapDrawable bitmapDrawable = (BitmapDrawable) mImageView.getDrawable(); if (null != bitmapDrawable) { Bitmap bitmap = bitmapDrawable.getBitmap(); int width = bitmap.getWidth(); int height = bitmap.getHeight(); int byteCount = bitmap.getByteCount(); System.out.println("----> width=" + width + ",height=" + height); System.out.println("----> byteCount=" + byteCount); } } }); }
輸出結果如下:
width=144,height=180,byteCount=103680
嗯哼,獲取到的圖片寬高和其原本的寬高一致。那麼這個byteCount又是怎麼算出來的呢?
Android系統在利用drawable中的圖片生成Bitmap時默認采用的色彩模式是Bitmap.Config.ARGB_8888;在該模式中一共有四個通道,其中A表示Alpha,R表示Red,G表示Green,B表示Blue;並且這四個通道每個各占8位即一個字節,所以合起來共計4個字節。於是可以算出:144*180*4=103680字節
現在將圖片移至drawable-hdpi中,運行後查看效果:
輸出結果如下:
width=288,height=360,byteCount=414720
哇哈,看到沒有呢?——圖片的寬和高都翻倍了,圖片所占的內存大小也隨之變大了4倍。
繼續嘗試,在將圖片移至drawable-ldpi中,運行後查看效果:
輸出結果如下:
width=576,height=720,byteCount=1658880
這就更明顯了,圖片的寬和高都變大了4倍,圖片所占的內存大小也隨之變大了16倍。
嗯哼,如果將圖片放入drawable-mdpi,drawable-xhdpi,drawable-xxxhdpi中也會發現類似的現象:圖片的寬高及其所占內存在按照比例放大或者縮小,詳情請參見下圖
既然已經看到了這個現象,那就再從源碼(Lollipop 5.0)角度來看看當加載drawable中的圖片時的具體實現
調用BitmapFactory中的的decodeResource()加載drawable文件夾裡的圖片,源碼如下:
public static Bitmap decodeResource(Resources res, int id, Options opts) { Bitmap bm = null; InputStream is = null; try { final TypedValue value = new TypedValue(); is = res.openRawResource(id, value); bm = decodeResourceStream(res, value, is, null, opts); } catch (Exception e) { } finally { try { if (is != null) is.close(); } catch (IOException e) { } } if (bm == null && opts != null && opts.inBitmap != null) { throw new IllegalArgumentException("Problem decoding into existing bitmap"); } return bm; }
在該方法中第6行調用openRawResource()後,value中就保存了該資源所在文件夾的destiny,這點和剛才的講解是一致的,不再贅述。在此之後,繼續執行decodeResourceStream()
調用decodeResourceStream( )方法
public static Bitmap decodeResourceStream(Resources res, TypedValue value, InputStream is, Rect pad, Options opts) { if (opts == null) { opts = new Options(); } if (opts.inDensity == 0 && value != null) { final int density = value.density; if (density == TypedValue.DENSITY_DEFAULT) { opts.inDensity = DisplayMetrics.DENSITY_DEFAULT; } else if (density != TypedValue.DENSITY_NONE) { opts.inDensity = density; } } if (opts.inTargetDensity == 0 && res != null) { opts.inTargetDensity = res.getDisplayMetrics().densityDpi; } return decodeStream(is, pad, opts); }
在該方法中有兩個非常重要的操作。
第一步:
為opts.inDensity賦值,請參見代碼第6-13行。
經過操作opts.inDensity會被賦值為120、160、240、320、480、640中的一個值
第二步:
為opts.inTargetDensity賦值,請參見代碼第14-16行。
經過操作opts.inTargetDensity會被賦值為手機屏幕的densityDpi
調用decodeStream()方法
在該方法中會調用decodeStreamInternal();它又會繼續調用nativeDecodeStream( ),該方法是native的;在BitmapFactory.cpp可見這個方法內部又調用了doDecode()它的核心源碼如下:
static jobject doDecode(JNIEnv*env,SkStreamRewindable*stream,jobject padding,jobject options) { ...... if (env->GetBooleanField(options, gOptions_scaledFieldID)) { const int density = env->GetIntField(options, gOptions_densityFieldID); const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID); const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID); if (density != 0 && targetDensity != 0 && density != screenDensity) { scale = (float) targetDensity / density; } } } const bool willScale = scale != 1.0f; ...... SkBitmap decodingBitmap; if (!decoder->decode(stream, &decodingBitmap, prefColorType,decodeMode)) { return nullObjectReturn("decoder->decode returned false"); } int scaledWidth = decodingBitmap.width(); int scaledHeight = decodingBitmap.height(); if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) { scaledWidth = int(scaledWidth * scale + 0.5f); scaledHeight = int(scaledHeight * scale + 0.5f); } if (willScale) { const float sx = scaledWidth / float(decodingBitmap.width()); const float sy = scaledHeight / float(decodingBitmap.height()); ...... SkPaint paint; SkCanvas canvas(*outputBitmap); canvas.scale(sx, sy); canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint); } ...... }
主要步驟分析如下:
第一步:
獲取opts.inDensity的值賦給density,請參見代碼第4行。
第二步:
獲取opts.inTargetDensity的值賦給targetDensity,請參見代碼第5行。
第三步:
計算縮放比scale,請參見代碼第8行。
從這裡也可以看出,這個縮放比scale就等於opts.inTargetDensity/opts.inDensity
第四步:
得到圖片原始的寬和高,請參見代碼第18-19行。
請注意此時的圖像在frameworks/base/core/jni/android/graphics/BitmapFactory.cpp中是一個SkBitmap
第五步:
依據scale計算縮放後SkBitmap的寬和高,請參見代碼第21-22行。
第六步:
計算SkBitmap的寬和高縮放的倍數,請參見代碼第25-26行。
在此得到寬的縮放倍數為sx, 高的縮放倍數為sy
第七步:
依據sx和sy縮放canvas,請參見代碼第30行。
第八步:
畫出圖片,請參見代碼第31行。
至此終於完成了doDecode()版的天龍八部。在梳理了整個過程之後不難發現:對於圖片縮放的比例其實還是scale即opts.inTargetDensity/opts.inDensity起了決定性的作用。
好吧,現在回過頭瞅瞅我掉進去的那個坑:我的手機華為P7其dpi值為480,有一張圖片我把它放到drawable-xxhdpi裡在手機上顯示出來是不失真的,非常合適;但是錯放到了drawable-xhdpi(其TypedValue的value值為320)後再次顯示時發現圖片被放大了,而且放大了480/320=1.5倍。既然圖片被放大了那麼該圖片所占的內存當然也變大了。
這也就解釋了我們有時遇到的類似困惑:為什麼圖片放在drawable-xxhdpi是正常的,但是放到drawable-mdpi後圖片不僅僅放大失真而且所占內存也大幅增加了。
至此,對於Andoid中常見的度量單位已經介紹完了;關於drawable的加載原理也做了一個完整分析。
在明白這些之後,我們再去談多分辨率的適配也就會多了一份從容和自信。
以前如果要做 Tab 分頁的話,必須要用一個很難用的 TabActivity,而且做出來的效果很差,彈性也很小忘了從什麼時候開始,Google release 了 Vie
支付寶的快捷支付Android版業務流程比較麻煩,出現的意外情況比較多.在此,簡單說下開發流程以及出現錯誤的解決方案; 1.注冊支付業務.這裡不在贅述.建立數據安全傳輸所
由於不是系統級的應用, 也沒有獲得ROOT權限, 所以自己實現任務管理器其實意義並不是很大, 就像沒有root的手機安裝了LBE這類的手機助手, 雖然也帶一鍵清理內存清理
最近研究Android應用的增量升級功能,期間涉及到了NDK開發的內容,整理記錄在此。先說幾個問題。一、NDK開發就是JNI開發啊,臥槽我原來都不知道啊,一直以為是兩個東