1. 程式人生 > >Android自定義控制元件系列八:詳解onMeasure()(二)--利用onMeasure測量來實現圖片拉伸永不變形,解決螢幕適配問題

Android自定義控制元件系列八:詳解onMeasure()(二)--利用onMeasure測量來實現圖片拉伸永不變形,解決螢幕適配問題

        上一篇文章詳細講解了一下onMeasure/measure方法在Android自定義控制元件時的原理和作用,參看博文:Android自定義控制元件系列七:詳解onMeasure()方法中如何測量一個控制元件尺寸(一),今天就來真正實踐一下,讓這兩個方法大顯神威來幫我們搞定圖片的螢幕適配問題。

使用ImageView會遇到的問題

        在Android應用中,都少不了圖片的顯示,ImageView,輪播圖,ViewPager等等,很多都是來顯示圖片的,比如一個廣告條的輪播效果,參看部落格:廣告條效果實現----ViewPager載入大圖片(LruCache)以及定時重新整理

,很多時候,我們都希望圖片能夠在寬度上填充父窗體,這樣比較符合人的審美觀點,但是問題就隨之而來了,那就是高度如何定義??先來看一個普通的ImageView的 Xml佈局檔案的定義:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation
    ="vertical" >  
  6.     <ImageView  
  7.         android:layout_width="fill_parent"  
  8.         android:layout_height="wrap_content"  
  9.         android:src="@drawable/recommend_39" />  
  10.     <TextView  
  11.         android:layout_width="wrap_content"  
  12.         android:layout_height="wrap_content"  
  13.         android:text
    ="描述文字資訊........................." />  
  14. </LinearLayout>  

為了方便檢視,我在ImageView下面又加上了一句描述的資訊的TextView,這時,父控制元件都是填充父窗體,而ImageView則是:橫向填充父窗體,縱向包裹內容;text都是包裹內容;那麼來看看顯示效果:


上面那個藍色的小框就是ImageView的範圍,這種效果一般都不會是我們想要的,那麼如果想要ImageView中的圖片能夠填滿ImageView的整個窗體怎麼辦?新增一個屬性:scaleType,如下:

  1. <ImageView  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="wrap_content"  
  4.     android:scaleType="fitXY"  
  5.     android:src="@drawable/recommend_39" />  

效果如圖:


可以看到,填是填滿了,但是也由於縱向拉伸而使圖片變形了。那要怎麼做呢?

我們仔細觀察一下,不難發現ImageView的縱向高度是包裹內容:wrapcontent,有些同學可能想到,能夠直接在這裡給ImageView一個特定的dip值,讓這個ImageView符合圖片的寬高比呢?這樣做無疑是可以的,但是卻不具有通用性。。。下面還是用上面的例子來講解:

我們先來實現一下,制定ImageView的高度,來達到讓這張圖片在這個特定模擬器下顯示比例正常的過程:

上面的圖片實際畫素的尺寸是:828*314,寬度和高度的比例大約是2.43,而我們這裡使用的模擬器的尺寸是480*800(單位是px,也就是畫素),也就是說寬度上的畫素是480,那麼我們要設定這個ImageView的高度為多少dip才能夠讓其正好符合2.43的比例呢。

Android裝置上px(畫素)、dpi(畫素密度)、dip(密度無關畫素)之間的關係:

這裡又要牽扯出另外一塊知識點,dip和dpi的聯絡:

解析度(Resolution):表示裝置螢幕上畫素點的總數,比如上面的模擬器,螢幕畫素尺寸是480(px)*800(px)

dpi(畫素密度):是指每英寸的畫素,所以同分辨率的兩個裝置,它們的dpi很可能不一樣;如果一個手機解析度5寸是1080*1920,而一個平板9.7寸解析度也是1080*1920,那麼 手機的dpi會比平板高出很多。

dp/dip:全稱是Density-independent pixel ,中文名是 “密度無關畫素”,也就是我們經常在xml檔案中寫的長度單位dp。為什麼叫做密度無關畫素呢,這其實是為了解決不同解析度裝置顯示效果統一的一個解決方案,試想,如果一個兩個手機螢幕都是一樣大小,比如5寸,A手機的解析度是 720*1280,而B手機的解析度是1080*1920;那麼如果我們想在上面顯示一個圖片解析度為:200*200的圖片,就會發現,在A手機上顯示的圖片,比B手機上顯示的圖片要小了很多;直觀的來看,A手機的寬度是720,顯示200*200的圖片差不多要佔據將近1/3的寬度,而反觀B手機,寬度是1080,顯示200*200的圖片,則只需要佔據1/5不到的寬度,而兩個手機的尺寸又都是5寸,所以就會在顯示同樣解析度的圖片時,產生大小的差異。這種差異明顯不是我們想要的。

所以dip/dp,密度無關畫素就應運而生;它是這樣規定的,dip與一個dpi(畫素密度)為160dpi的裝置的px(畫素)值是相等的,而對於其他畫素密度的裝置,則依據轉換公式來計算對應的dip值,這個公式是根據dpi(相當於比例),來轉換px(畫素)和dip(密度無關畫素)的:

  1. px = dip * (dpi / 160)  
  2. dip = px / (dpi / 160)  

經過上面的轉換之後,由於dip和px的轉換是按照比例來的,而這個比例又是dpi/160,而dpi又是根據各個裝置的解析度和尺寸的比例得來的,所以使用相同的dip來設定的尺寸的控制元件,在相同尺寸大小的裝置上,不論裝置的解析度是多少,它們顯示的大小都會是一樣的。

計算ImageView的合適高度的方法

有了上面的知識之後,我們就可以來轉換一下我們的ImageView的大小了:

首先,圖片的寬高比是2.43,而模擬器螢幕的寬度是480px,於是計算得到圖片應該顯示的高度是:480/2.43=197.53px(畫素),但是一般時候,我們在xml檔案中,設定的高度單位都是dip,所以這裡要需要使用上面的轉換公式:dip = px / (dpi / 160),而在這個模擬器的引數上可以查詢到,它的dpi是240,所以計算得到高度應該是:

197.53/(240/160)=131.68(dip),約等於132dp,於是我們將上面的ImageView的高度設定成132dp:

  1. <ImageView  
  2.     android:layout_width="fill_parent"  
  3.     android:layout_height="132dp"  
  4.     android:scaleType="fitXY"  
  5.     android:src="@drawable/recommend_39" />  

再來看一下顯示效果:


這樣顯示比例就完全正常了。

但是這樣問題就解決了嗎?答案是沒有,我們不妨來換一個模擬器來顯示,這次選用Nexus7的模擬器,解析度為1200*1920,dpi為320,ImageView的引數不變,再來看看效果:


會發現圖片被拉長了,這是為什麼 呢,我們可以簡單的再算一下:

nexus7 寬度為1200px,而dpi為320,圖片比例為2.43,那麼應該設定ImageView的高度dp值是:1200/2.43/(320/160)=246.91dp,而我們現在的高度卻還是之前的132dp,當然會發現被拉伸了。

那怎麼辦,有點讓人抓狂!!!

重寫onMeasure方法,重新測量控制元件高度,實現多種螢幕下自適應圖片顯示

其實辦法是有的,思路就是讓控制元件(ImageView)自己根據不同的裝置幫我們來計算這個高度,而不需要我們自己去計算,那要怎麼做呢?就得用到上一篇博文中說到的onMeasure方法了,對measure/onMeasure方法不熟悉的同學,可以再去看一下Android自定義控制元件系列七:詳解onMeasure()方法中如何測量一個控制元件尺寸(一)和下面就開始:

首先要明確的一點就是,自定的view在呼叫view.measure()之前,是得不到控制元件的寬和高的,下面就一步步來寫:

思路是首先寫一個SmartImageView來繼承自ImageView,並且新增相應的構造:

  1. package com.example.imageviewdemo;  
  2. import android.content.Context;  
  3. import android.util.AttributeSet;  
  4. import android.widget.ImageView;  
  5. /** 
  6.  * @author : 苦咖啡 
  7.  *  
  8.  * @version : 1.0 
  9.  *  
  10.  * @date :2015年4月14日 
  11.  *  
  12.  * @blog : http://blog.csdn.net/cyp331203 
  13.  *  
  14.  * @desc : SmartImageView,能根據給定的圖片比例,自動調整寬高,解決拉伸變形的螢幕適配問題 
  15.  */  
  16. public class SmartImageView extends ImageView {  
  17.     public SmartImageView(Context context, AttributeSet attrs,  
  18.             int defStyleAttr, int defStyleRes) {  
  19.         super(context, attrs, defStyleAttr, defStyleRes);  
  20.     }  
  21.     public SmartImageView(Context context, AttributeSet attrs, int defStyle) {  
  22.         super(context, attrs, defStyle);  
  23.     }  
  24.     public SmartImageView(Context context, AttributeSet attrs) {  
  25.         super(context, attrs);  
  26.     }  
  27.     public SmartImageView(Context context) {  
  28.         super(context);  
  29.     }  
  30. }  


然後在SmartImageView中,新增一個float型別的成員變數ratio作為圖片的比例值,並且給它暴露一個setter方法,以便於設定圖片比例。

  1. /** 圖片寬和高的比例 */  
  2. private float ratio = 2.43f;  
  3. public void setRatio(float ratio) {  
  4.     this.ratio = ratio;  
  5. }  


然後我們來重寫onMeausre方法,如下:

  1. @Override  
  2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  3.     // 父容器傳過來的寬度方向上的模式  
  4.     int widthMode = MeasureSpec.getMode(widthMeasureSpec);  
  5.     // 父容器傳過來的高度方向上的模式  
  6.     int heightMode = MeasureSpec.getMode(heightMeasureSpec);  
  7.     // 父容器傳過來的寬度的值  
  8.     int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft()  
  9.             - getPaddingRight();  
  10.     // 父容器傳過來的高度的值  
  11.     int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingLeft()  
  12.             - getPaddingRight();  
  13.     if (widthMode == MeasureSpec.EXACTLY  
  14.             && heightMode != MeasureSpec.EXACTLY && ratio != 0.0f) {  
  15.         // 判斷條件為,寬度模式為Exactly,也就是填充父窗體或者是指定寬度;  
  16.         // 且高度模式不是Exaclty,代表設定的既不是fill_parent也不是具體的值,於是需要具體測量  
  17.         // 且圖片的寬高比已經賦值完畢,不再是0.0f  
  18.         // 表示寬度確定,要測量高度  
  19.         height = (int) (width / ratio + 0.5f);  
  20.         heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,  
  21.                 MeasureSpec.EXACTLY);  
  22.     } else if (widthMode != MeasureSpec.EXACTLY  
  23.             && heightMode == MeasureSpec.EXACTLY && ratio != 0.0f) {  
  24.         // 判斷條件跟上面的相反,寬度方向和高度方向的條件互換  
  25.         // 表示高度確定,要測量寬度  
  26.         width = (int) (height * ratio + 0.5f);  
  27.         widthMeasureSpec = MeasureSpec.makeMeasureSpec(width,  
  28.                 MeasureSpec.EXACTLY);  
  29.     }  
  30.     super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  31. }  


對於onMeasure方法,有幾點需要注意的:

1、父容器傳過來的兩個引數widthMeasureSpec和heightMeasureSpec,通過MeasureSpec.getMode()來獲取引數中的模式,與控制元件的填充方式都是有對應關係的,這在上一篇博文:Android自定義控制元件系列七:詳解onMeasure()方法中如何測量一個控制元件尺寸(一)中也有提到過

        ①xml佈局檔案中的fill_parent或具體值,或者是直接設定控制元件的LayoutParams中的width和height的具體值或者LayoutParams.FILL_PARENT填充父容器方式,都會對應讓上面通過getMode獲取引數中的模式為:MeasureSpect.EXACTLY,代表精確取值,因為除了直接指定值之外,填充父容器,也是精確值

        ②xml佈局檔案中設定wrap_content方式或者是在程式碼中設定LayoutParams.WRAP_CONTENT方式,都會讓getMode變成MeasureSpect.AT_MOST

2、對於父容器傳過來的高度或者寬度的值,不一定就是控制元件想要的寬度或者高度的值,這是因為模式不一樣,這個值代表的意義也不一樣,所以才會需要通過測量來改變高度或者寬度的值來達到想要的效果。

其中,如果是模式是EXACTLY,那麼傳過來的值就是具體指,也可以說是父容器想要我們的控制元件變成這個具體的大小。

但是模式如果是AT_MOST,那麼傳過來的值,就不會是具體的值,一般會是一個最大值,因為AT_MOST代表,不超過多少,那麼這個值就是不超過的上限。

3、可以看到我們通過拿到父容器傳過來的高度,寬度的模式和值,然後經過兩種if-else判斷來重新測量值的大小,這兩種判斷的依據就是:

        ①當寬度確定時(寬度為EXACTLY),高度模式不是EXACTLY時(也即高度不確定時),高度按照ratio的比例來重新測量

        ②當高度確定時(高度為EXACTLY),高度模式不是EXACTLY時(也即高度不確定時),寬度按照ratio的比例來重新測量

4、在測量完畢之後,因為已經得到了想要的寬度或者高度的具體的精確的值,我們再通過MeasureSpec.makeMeasureSpec()方法來呼叫精確的值和精確的模式,來合成一個寬度/高度方向上的合成值,最後將合成好的值傳遞給super.onMeasure(widthMeasureSpec, heightMeasureSpec);設定控制元件為我們想要的大小。

然後我們就可以在XML佈局檔案中,將之前的ImageView改成:com.example.imageviewdemo.SmartImageView

然後在程式碼中將其通過findviewbyid拿到它的物件,然後通過setRatio來設定圖片的比例,如下:

  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical" >  
  6.     <com.example.imageviewdemo.SmartImageView  
  7.         android:layout_width="fill_parent"  
  8.         android:layout_height="wrap_content"  
  9.         android:scaleType="fitXY"  
  10.         android:id="@+id/siv"  
  11.         android:src="@drawable/recommend_39" />  
  12.     <TextView  
  13.         android:layout_width="wrap_content"  
  14.         android:layout_height="wrap_content"  
  15.         android:text="描述文字資訊........................." />  
  16. </LinearLayout>  

  1. package com.example.imageviewdemo;  
  2. import android.app.Activity;  
  3. import android.os.Bundle;  
  4. public class MainActivity extends Activity {  
  5.     @Override  
  6.     protected void onCreate(Bundle savedInstanceState) {  
  7.         super.onCreate(savedInstanceState);  
  8.         setContentView(R.layout.activity_main);  
  9.         // 拿到SmartImageView物件  
  10.         SmartImageView siv = (SmartImageView) findViewById(R.id.siv);  
  11.         // 設定ratio的值  
  12.         siv.setRatio(2.43f);  
  13.     }  
  14. }  

經過上面之後,我們會發現,不論在什麼螢幕下,不論在橫屏還是豎屏,都能以正確的比例顯示圖片了,


效果圖:

        

                       240*320/2.7寸裝置                                                                  nexus7 1200*1920裝置


                                     androidTV(1920*1080)橫屏顯示裝置

最後再留一個小地方,就是要顯示圖片的ratio,這個可以有多種方式,一種是從伺服器傳過來時,伺服器指定了,那麼我們可以直接拿到,然後設定好即可;然後是自己通過測量BitMap的寬高來確定比例,也是可以的。

設定ratio的方式可以像上面的呼叫setRatio()方法,也可以使用自定義屬性,在XML檔案中直接確定,關於自定義屬性,由於不是文字重點,不瞭解的同學可以去看看專欄的這篇文章:Android自定義控制元件系列四:自定義開關按鈕(三)--- 自定義屬性,就會明白了。

之後會帶來自定義View的另一個方法:onLayout方法的研究,敬請期待,謝謝!

相關推薦

Android定義控制元件系列onMeasure()()--利用onMeasure測量實現圖片永不變形解決螢幕問題

        上一篇文章詳細講解了一下onMeasure/measure方法在Android自定義控制元件時的原理和作用,參看博文:Android自定義控制元件系列七:詳解onMeasure()方法中如何測量一個控制元件尺寸(一),今天就來真正實踐一下,讓這兩個方法大顯神威來幫我們搞定圖片的螢幕適配問題。

Android定義控制元件系列onMeasure()方法中如何測量一個控制元件尺寸(一)

自定義view/viewgroup要重寫的幾個方法:onMeasure(),onLayout(),onDraw()。(不熟悉的話可以檢視專欄的前幾篇文章:)。         今天的任務就是詳細研究一下protected void onMeasure(int wid

Android定義控制元件系列如何測量控制元件尺寸

測量控制元件尺寸(寬度、高度)是開發自定義控制元件的第一步,只有確定尺寸後才能開始畫(利用canvas在畫布上畫,我們所使用的控制元件實際上都是這樣畫上去的)。當然,這個尺寸是需要根據控制元件的各個部分計算出來的,比如:padding、文字大小,間距等。非容器控制元件的onM

Android定義控制元件系列利用新增定義佈局搞定觸控事件的分發解決組合介面中特定控制元件響應特定方向的事件

        這個例子是比較有用的,基本上可以說,寫完這一次,以後很多情況下,直接拿過來addView一下,然後再addInterceptorView一下,就可以輕輕鬆鬆的達到組合介面中特定控制元件來響應特定方向的觸控事件了。         在寫Android應用

Android定義控制元件系列Android如何實現老版優酷客戶端三級環形選單

轉載連結:http://blog.csdn.net/cyp331203/article/details/40423727 先來看看效果: 一眼看上去好像還挺炫的,感覺比較複雜。。。實際上並不難,下面我們來看看如何實現: 基本素

Android定義控制元件系列onMeasure()方法中如何測量一個控制元件尺寸(一)

轉載請註明出處:http://blog.csdn.net/cyp331203/article/details/45027641 今天的任務就是詳細研究一下protected void onMeasure(int widthMeasureSpec, int he

Android定義控制元件系列定義開關按鈕(一)

這一次我們將會實現一個完整純粹的自定義控制元件,而不是像之前的組合控制元件一樣,拿系統的控制元件來實現;計劃分為三部分:自定義控制元件的基本部分,和自定義控制元件的自定義屬性; 下面就開始第一部分的編寫,本次以一個定義的開關按鈕為例,下面就開始吧: 先看看效果,一個點選開

Android定義控制元件系列(三)—底部選單(上)

今天我們封裝一個底部的選單欄,這個大多數的應用都會用到,因此我們來自定義,方便以後專案的使用。 該控制元件的實現將分上下篇來介紹,先來看一個選單欄的子控制元件–MenuItemM,這個控制元件有什麼用呢?我們來看下一些主流app上的一些控制元件,如:

Android定義控制元件系列案例【四】

案例效果: 模擬器上執行有些鋸齒,真機上和預期一樣好 案例分析: 看效果,第一直覺肯定是Android原生態控制元件中沒有這樣的控制元件實現這種效果,自然想到應該需要自定義控制元件了,沒錯,這就是通過自定義控制元件來繪製的一個圓環進度條。仔細分析發現這個效果的進度條應該

Android定義控制元件系列案例【一】

Android自定義控制元件的重要性就不多說了,總之是技術進階,面試常見,高薪必備。 本篇博文的目的很簡單,就是希望通過自定義控制元件來解決一個常見需求點,從而感受一下自定義控制元件的魅力與強大。 案

android定義控制元件系列教程----繼承ViewGroup實現帶阻力效果的可回彈的SrollView

package com.example.scolview; import android.content.Context; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracke

Android定義控制元件系列()—icon+文字的多種效果實現

今天給大家帶來一個很簡單但是很常用的控制元件ButtonExtendM,在開發中我們經常會用到圖片加文字的組合控制元件,像這樣: 以上圖片都是從微信上擷取的。(暫時沒有找到icon在下,文字在上的例子) 下面我們通過一個控制元件來實現上下左右全部

Android 定義控制元件打造流佈局實現熱門搜尋標籤

具體實現 1,自定義一個類繼承GridView /** * 自定義流佈局 * @author zhouyou */ public class ZFlowLayout extends ViewGroup{ // 儲存所有子View priva

Android定義控制元件動畫類---逐幀動畫AnimationDrawable

1:概述             Android動畫包括View Animation(檢視動畫)和Property Animator(屬性動畫),而View Animation包括Tween An

Android定義控制元件開發系列(三)——仿支付寶六位支付密碼輸入頁面

        在移動互聯領域,有那麼幾家龍頭一直是我等學習和追求的目標,比如支付寶、微信、餓了麼、酷狗音樂等等,大神舉不勝舉,他們設計的介面、互動方式已經培養了中國(有可能會是世界)民眾的操作習慣:舉個小例子,對話方塊“確定”按鈕的左右位置就很有學問,如果大家都是左邊取消

Android定義控制元件Android L控制元件點選水波紋的實現(原始碼 + Demo)

Demo: 一、控制元件的流程: 大致上如下,實際是有些偏差的大家可以自己畫畫 RevealLayout()--->init()--->onMeasure()--->onLayout()--->onDraw()--->dispat

Android定義控制元件動畫類----alpha、scale、translate、rotate、set的xml屬性及用法

二、下面我們逐個講講每個標籤的屬性及用法 1、scale標籤——調節尺寸 1> 自有屬性 scale標籤是縮放動畫,可以實現動態調控制元件尺寸的效果,有下面幾個屬性: android:fromXScale    起始的X方向上相對自身的縮放比例,浮點值,比如1.0代表自身無變化,

Android定義控制元件將ViewPager封裝自己的TabPager控制元件

用途 最近專案頁面中經常出現諸如下圖的控制元件,如果為每個頁面分別寫一個將會造成非常多的重複程式碼,不利於專案的閱讀和維護,也會使專案變得非常凌亂。所以,對於這種情況我們可以進行一定的抽取,傳入相關資料後自動顯示到控制元件上。

Android定義控制元件開發系列(一)——第一次動手做定義控制元件

        Android系統提供的控制元件多種多樣,以至於很多初學者經常忘了還有這樣那樣的控制元件沒用過甚至沒聽過。儘管如此,但是系統控制元件大多比較死板,而且不夠美觀,很多多樣化的顯示或是互動

Android 定義控制元件起步定義TextView

首先我們看一下我們要達到的效果: 在點選我們自定義的View時文字會隨機改變。好,現在我們開始… 很多的Android入門程式猿來說對於Android自定義View,可能都是比較恐懼的,但是這又是高手進階的必經之路,所有準備在自定義View上面花