android - px(像素)、dpi(像素密度)、dip(密度無關像素)之間的關系


使用ImageView會遇到的問題

 

        在Android應用中,都少不了圖片的顯示,ImageView,輪播圖,ViewPager等等,很多都是來顯示圖片的,很多時候,我們都希望圖片能夠在寬度上填充父窗體,這樣比較符合人的審美觀點,但是問題就隨之而來了,那就是高度如何定義??先來看一個普通的ImageView的 Xml布局文件的定義:

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    
    
    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/play"
        android:background="#0000ff"
        />
    
    
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="描述文字信息..."
        />
    
</LinearLayout>

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

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

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    
    
    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/play"
        android:background="#0000ff"
        android:scaleType="fitXY"
        />
    
    
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="描述文字信息..."
        />
    
</LinearLayout>

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

 

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

 

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

 

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

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(密度無關像素)的:

px = dip * (dpi / 160)

dip = px / (dpi / 160)

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

計算ImageView的合適高度的方法

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

 

首先,圖片的寬高比是2.63,而模擬器屏幕的寬度是768px,於是計算得到圖片應該顯示的高度是:768/2.63=292.015px(像素),但是一般時候,我們在xml文件中,設定的高度單位都是dip,所以這里要需要使用上面的轉換公式:dip = px / (dpi / 160),而在這個模擬器的參數上可以查詢到,它的dpi是320,所以計算得到高度應該是:

292/(320/160)=146(dip),於是我們將上面的ImageView的高度設置成146dp:

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    
    
    <ImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="146dip"
        android:src="@drawable/play"
        android:background="#0000ff"
        android:scaleType="fitXY"
        />
    
    
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="描述文字信息..."
        />
    
</LinearLayout>

再看一下顯示效果:

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

 

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

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

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

 

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

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

其實辦法是有的,思路就是讓控件(ImageView)自己根據不同的設備幫我們來計算這個高度,而不需要我們自己去計算,那要怎么做呢?

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

 

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

package com.example.test.view;


import android.content.Context;
import android.util.AttributeSet;
import android.widget.ImageView;

public class SmartImageView extends ImageView {

    
    // initWithFrame
    public SmartImageView(Context context) {
        super(context);
    }

    // initWithCoder awakeFromXib
    public SmartImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    
}

然后在SmartImageView中,添加一個float類型的成員變量ratio作為圖片的比例值,並且給它暴露一個setter方法,以便於設置圖片比例。

/**
     * 圖片寬高比
     * 
     */
    private float ratio = 2.63f;
    
    public void setRatio(float ratio) {
        this.ratio = ratio;
    }

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

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        
        // 寬度方向上的測量模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        // 高度方向上的測量模式
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        
        // paddingLeft 類似於 iOS中的 UIEdgeInsert
        int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
        int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
        
        /**
         * 判斷依據
         * 寬度為Exactly,也就是填充父窗體或者指定寬度
         * 且高度不為Exactly,代表設置的既不是fill_parent也不是具體的值,需要具體測量
         * 且圖片寬度比已經賦值完畢,不再是0.0f
         * 表示寬度確定,需要確定高度
         * MeasureSpec類似於ios中的CGSize,比CGSize多了一個mode屬性
         */
        if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY && ratio != 0.0f) {
            height = (int) (width / ratio + 0.5f); // 高度實際值(px)
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
        } else if (widthMode != MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY && ratio != 0.0f) {
            // 判斷依據與上面相反,表示高度確定,需要測量寬度
            width = (int) (height * ratio + 0.5f);
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        }
        
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

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

1、父容器傳過來的兩個參數widthMeasureSpec和heightMeasureSpec,通過MeasureSpec.getMode()來獲取參數中的模式,與控件的填充方式都是有對應關系的

        ①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來設定圖片的比例,如下:

<LinearLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    >
    
    
    <com.example.test.view.SmartImageView
        android:id="@+id/iv_img"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:src="@drawable/play"
        android:background="#0000ff"
        />
    
    
    <TextView 
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="描述文字信息..."
        />
    
</LinearLayout>
package com.example.test;

import android.app.Activity;
import android.os.Bundle;
import com.example.test.view.SmartImageView;

public class MainActivity extends Activity {

    
    private SmartImageView iv_img;
    
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        
        iv_img = (SmartImageView) findViewById(R.id.iv_img);
        iv_img.setRatio(2.63f);
        
    /*    *//**
         * 獲取屏幕尺寸參數
         * ios獲取方式 [UIScreen mainScreen].bounds
         *//*
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        

        Log.i("test", "寬度(px): "+metrics.widthPixels+" 高度(px): "+metrics.heightPixels+" dpi: "+metrics.densityDpi+" dpi/160: "+metrics.density);*/
        
    }

    
}

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


效果圖:

(豎屏)

(橫屏)

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

本文轉載自:http://blog.csdn.net/cyp331203/article/details/45038329

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM