1. 程式人生 > >Sdk介面UI開發自動適配螢幕技巧

Sdk介面UI開發自動適配螢幕技巧

前面有兩篇分別是關於Sdk安全方面以及開發注意事項-Sdk迭代開發設計需要考慮的方面總結, 這篇繼續講Sdk開發相關內容-Sdk介面UI開發中圖片自適配。在apk開發中我們不會太關注這個圖片適配,因為系統會自動適配,而導致圖片出現失真模糊另說。這裡就是要從圖片載入到記憶體時,從圖片bitmap加載出來具體大小是多少講起。後文也補充了關於控制元件尺寸的適配。

圖片Bitmap載入到記憶體時具體大小

一張36*36的account_symbol_boy.png圖片放在專案drawable-xhdpi目錄下,專案執行在1080*1920解析度的手機上,當這張圖片加載出來所佔用的記憶體是多少呢?大家很可能認為記憶體大小=圖片寬度高度

一畫素所佔位元組數=36*36*4=6400byte。實際多少,執行下看程式碼列印結果:

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.account_symbol_boy);

    int count = bitmap.getByteCount();
    Log.i("chuan", "count="+count);
    Log.i("chuan", "evaluate count="+36*36*4);


09-06 13:10:32.014 7379-7379/com.yzf0813.test
I/chuan: count=8836 09-06 13:10:32.014 7379-7379/com.yzf0813.test I/chuan: evaluate count=5184

可以看到結果8836並非預計的5184,這就說明在解析度更高系統上,圖片大小存在放大適配,顯示效果也更適應螢幕解析度。

如果是執行在720p的手機上,佔用記憶體大小是多少呢?結果如下:

09-06 13:45:04.992 3446-3446/com.yzf0813.test I/chuan: count=5184
09-06 13:45:04.993 3446-3446/com.yzf0813.test I/chuan: evaluate count=5184

可以看到實際結果和預計的是一樣的。圖片放在drawable-xhdpi目錄下對應了denstyDpi為320剛好和720p手機一致,而在1080p手機上,其denstiyDpi為420,可以看到大小被放大。那系統是如何做到的呢,直接看原始碼:

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) {
            /*  do nothing.
                If the exception happened on open, bm will be null.
                If it happened on close, bm is still valid.
            */
        } finally {
            try {
                if (is != null) is.close();
            } catch (IOException e) {
                // Ignore
            }
        }
          。。省略。。。

        return bm;

}

用的是 deceResource 方法讀取 drawable 目錄下面的圖片,該方法本質上就兩步:
1)讀取原始資源,這個呼叫了 Resource.openRawResource 方法,這個方法呼叫完成之後會對 TypedValue 進行,其中包含了原始資源的 density 等資訊,這個density就是圖片放置的解析度相關的目錄下決定的,如上文中放在drawable-xhdpi目錄下對應的獲取到的圖片density就是320。
2)再呼叫 decodeResourceStream 對原始資源資料流進行解碼和適配螢幕的解析度,具體原始碼如下。

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
            InputStream is, Rect pad, Options opts) {

        if (opts == null) {
            opts = new Options();
        }

        //跟據對原始圖片解析出來的typeValue中的density值來對opts.inDefu'zhiity 賦值
        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;
            }
        }

        //對opts目標density賦值為當前手機螢幕的解析度
        if (opts.inTargetDensity == 0 && res != null) {
            opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
        }

        return decodeStream(is, pad, opts);
    }

從上述圖片很明顯看到對解碼引數Option的inDensity 和inTargetDensity 進行賦值並傳入decodeStream(is, pad, opts),那這兩個引數如何對原始圖片啟動適配作用呢,繼續看到原始碼,走到本地方法中,跳過一些函式,直接看關鍵方法doDecode 函式的程式碼:

BitmapFactory.cpp

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);//對應xhdpi的時候,是320
        const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);//華為的1080p手機的為420
        const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
        if (density != 0 && targetDensity != 0 && density != screenDensity) {
       //這裡就用到傳入的引數Option的inDensity和inTargetDensity
            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");
}
//這裡這個deodingBitmap就是解碼出來的bitmap,大小是圖片原始的大小
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());

    // TODO: avoid copying when scaled size equals decodingBitmap size
    SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
    // FIXME: If the alphaType is kUnpremul and the image has alpha, the
    // colors may not be correct, since Skia does not yet support drawing
    // to/from unpremultiplied bitmaps.
    outputBitmap->setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
            colorType, decodingBitmap.alphaType()));
    if (!outputBitmap->allocPixels(outputAllocator, NULL)) {
        return nullObjectReturn("allocation failed for scaled bitmap");
    }

    // If outputBitmap's pixels are newly allocated by Java, there is no need
    // to erase to 0, since the pixels were initialized to 0.
    if (outputAllocator != &javaAllocator) {
        outputBitmap->eraseColor(0);
    }

    SkPaint paint;
    paint.setFilterLevel(SkPaint::kLow_FilterLevel);

    SkCanvas canvas(*outputBitmap);
    canvas.scale(sx, sy);
    canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
}
......
}

結論:

從上文中可以看到先載入了原始圖片,然後根據Option的兩個density引數來計算scale比例,利用canvas進行矩陣變化得到最終的outBitmap。那麼從中可以看到是否進行縮放就是依賴Option的兩個引數值,故我們要適配也抓住這點來在Sdk介面開發中獲取自動適配過的bitmap。

Sdk中圖片自動適配螢幕解析度

Sdk介面開發有兩種方式,一種就是也包括res資源目錄,加入到目標專案中進行便宜,這種沒有啥特殊,和Apk開發介面一樣,都是自動適配。接下來要講的是沒有res目錄進行編譯,圖片資源是放在assets目錄下不會被系統編譯生產相關資源id。這種情況下系統自帶的圖片適配就自動使用不起來,那我們就是要做到如此,根據上文分析我們知道了關鍵原理,那麼接下來獲取bitmap是如何做的呢。也是兩部:
1)首先讀取assets目錄下的圖片流

 AssetManager assetManager = context.getAssets();
        InputStream is = null;
        try {
            is = assetManager.open( srcFileName);
        } catch (IOException e) {
        }

2)就是獲取當前的圖片解析度以及手機螢幕的分辨並對Option進行賦值。當前的圖片解析度,在UI切圖時就決定好了,並且我們只用一套圖。

 private static Bitmap getFitScaledBitmap(Context context, InputStream is) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inDensity = DimenUtil.DEFAULT_DENSITY;
        options.inTargetDensity = context.getResources().getDisplayMetrics().densityDpi;
        return BitmapFactory.decodeStream(is, null, options);
    }

這樣我們就可以獲取自動適配的圖片了。當然這個只是Bitmap,很多情況下我們載入的都是.9圖片,在使用的事情都是drawable物件。所以統一使用Drawable createFromResourceStream(Resources res, TypedValue value,
InputStream is, String srcName)來完成載入。這時候就需要對value.denstiy引數賦值了,否則加載出來的就是原始圖大小了。

這麼多就結束了,實現很簡單,關鍵是要知道為何,原理是什麼!

順便講下UI尺寸適配,上文都是圖片,那麼UI給的尺寸也是如此,要自動適配到各個解析度螢幕上。在正常開發中我們一樣不需要處理這些,但在這種sdk介面開發時,需要自己適配尺寸。系統在獲取res下尺寸或佈局中設定的尺寸時都存在了一個轉換。舉個例子-testView字型大小尺寸:

TextView.class

                case com.android.internal.R.styleable.TextAppearance_textSize:
                    textSize = appearance.getDimensionPixelSize(attr, textSize);
                    break;


轉到TypedArray來獲取px畫素大小,在程式碼統一都是畫素。

TypedArray.class

 public int getDimensionPixelSize(@StyleableRes int index, int defValue) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }

        final int attrIndex = index;
        index *= AssetManager.STYLE_NUM_ENTRIES;

        final int[] data = mData;
        final int type = data[index+AssetManager.STYLE_TYPE];
        if (type == TypedValue.TYPE_NULL) {
            return defValue;
        } else if (type == TypedValue.TYPE_DIMENSION) {
            return TypedValue.complexToDimensionPixelSize(
                data[index+AssetManager.STYLE_DATA], mMetrics);
        } else if (type == TypedValue.TYPE_ATTRIBUTE) {
            final TypedValue value = mValue;
            getValueAt(index, value);
            throw new UnsupportedOperationException(
                    "Failed to resolve attribute at index " + attrIndex + ": " + value);
        }

        throw new UnsupportedOperationException("Can't convert value at index " + attrIndex
                + " to dimension: type=0x" + Integer.toHexString(type));
    }

字型大小資料型別為TypedValue.TYPE_DIMENSION ,可以看到用applyDimension來獲取具體float大小
進行對應轉換,關鍵就是DisplayMetrics metrics的density為螢幕的分別率密度

TypedValue.class

 public static int complexToDimensionPixelSize(int data,
            DisplayMetrics metrics)
    {
        final float value = complexToFloat(data);
        final float f = applyDimension(
                (data>>COMPLEX_UNIT_SHIFT)&COMPLEX_UNIT_MASK,
                value,
                metrics);
        final int res = (int)(f+0.5f);
        if (res != 0) return res;
        if (value == 0) return 0;
        if (value > 0) return 1;
        return -1;
    }

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

故在尺寸應用時候,可以直接利用TypedValue中的對應applyDimension方法