問題
為什么 ImageView 是155x155,而用 Fresco 加載的 Bitmap 卻是 512x512 呢
先看下 ViewHolder 的布局信息
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_marginBottom="10dp"
android:layout_marginTop="10dp"
android:orientation="horizontal">
<include
android:id="@+id/layout_1"
layout="@layout/unit_app_download"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<include
android:id="@+id/layout_2"
layout="@layout/unit_app_download"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<include
android:id="@+id/layout_3"
layout="@layout/unit_app_download"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<include
android:id="@+id/layout_4"
layout="@layout/unit_app_download"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
unit_app_download.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<org.qiyi.basecore.widget.QiyiDraweeView
android:id="@+id/poster"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop" />
<RelativeLayout
android:id="@+id/meta_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/poster">
...
</RelativeLayout>
</RelativeLayout>
基本就是 4個 QiyiDraweeView 4等分屏幕寬度(我的手機是1280x720,720/4=180,為了精簡展示,布局里一些 margin 等信息被我去掉)
布局這樣的寫法,在 setResizeOptions() 的時候,在控件未渲染的時候,獲取的寬高都是0( layout_width, layout_height 不是固定數值的都獲取不到,這個問題不是 Fresco 特有的,使用 UniversalImageLoader 也要面對這個問題)
public class QiyiDraweeView extends SimpleDraweeView {
....
private ResizeOptions getResizeOption() {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
int width = (layoutParams != null && layoutParams.width > 0) ? layoutParams.width : getScreenWidth();
int height = (layoutParams != null && layoutParams.height > 0) ? layoutParams.height : 1;
return new ResizeOptions(width, height);
}
public void setImageURI(final Uri uri) {
...
ImageRequest request = ImageRequestBuilder
.newBuilderWithSource(uri)
.setResizeOptions(getResizeOption())
.setImageDecodeOptions(ImageDecodeOptions.newBuilder().setDecodePreviewFrame(true).build())
.build();
...
}
}
解決
對我們來說,最容(甩)易(鍋)的解決方法就是讓服務端把這12張512x512的圖都縮小一些,但這是治標不治本,而且這些 url 還可能在其他地方使用
未渲染的時候,得不到 width 和 height,也得不到 measuredWidth 和 measuredHeight,那能不能在 measure 之后再去獲取寬高,獲取圖片呢? View 的確有提供這樣一個方法
public void setImageURI(final Uri uri) {
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
setImageURI(uri);
return true;
}
});
}
在 onPreDraw() 里就能獲取到寬高了。但是,這個從開始監聽到回調 onPreDraw() 經過了1秒多,這樣就相當于延遲1秒多才去獲取圖片,這就得不償失了,只能另尋方法了
那我們能不能在 getResizeOption() 的時候,如果獲取不到寬高,就自己去調用 measure() 呢?這樣就能得到 measuredWidth 和 measuredHeight
public final void measure(int widthMeasureSpec, int heightMeasureSpec)
但應該傳入多大的寬高呢?不用擔心,我們只要在當前位置 getRootView(),就能得到最上層的 View 了,調用 getRootView().measure() 傳入屏幕寬度和高度(我們估算的話,當然是讓最外層的 View 充滿屏幕,這樣才能保證估算出來的寬高不會小于實際寬高),之后當前 View 就能得到一個估算的寬高
private ResizeOptions getResizeOption() {
ViewGroup.LayoutParams layoutParams = getLayoutParams();
int width = (layoutParams != null && layoutParams.width > 0) ? layoutParams.width : getWidthOrMaxWidth();
int height = (layoutParams != null && layoutParams.height > 0) ? layoutParams.height : getHeightOrMaxHeight();
if (width <= 0 || height <= 0) {
getRootView().measure(MeasureSpec.makeMeasureSpec(getScreenWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(getScreenHeight(), MeasureSpec.EXACTLY));
if (width <= 0) {
width = getMeasuredWidth();
if (width <= 1) {//ImageView onMeasure() 中會判斷 width height,如果 <=0,就=1
width = getScreenWidth();
}
}
if (height <= 0) {
height = getMeasuredHeight();
}
}
return new ResizeOptions(width, height);
}
這樣改寫后,得到的 measuredWidth 和 measuredHeight 就是155
那 getRootView().measure() 的性能如何?
如果 ViewGroup 節點下有 N 個 ImageView,當第一個 ImageView 要獲取寬高的時候,因為獲取不到而執行 getRootView().measure(),那么后續的 N-1 個 ImageView 就可以直接獲取到寬高了,不會重復 measure
getWidthOrMaxWidth() 是參考 UniversalImageLoader 的代碼,其加入了 maxWidth 和 maxHeight 屬性的支持,就是當 layoutParams.width <= 0 時(不是固定值),去獲取 maxWidth,如果有配置則使用 maxWidth
ImageLoader.java
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
...
if (targetSize == null) {
targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
}
...
}
ImageSizeUtils.java
public static ImageSize defineTargetSizeForView(ImageAware imageAware, ImageSize maxImageSize) {
int width = imageAware.getWidth();
if (width <= 0) width = maxImageSize.getWidth();
int height = imageAware.getHeight();
if (height <= 0) height = maxImageSize.getHeight();
return new ImageSize(width, height);
}
ImageViewAware.java
@Override
public int getWidth() {
int width = super.getWidth();
if (width <= 0) {
ImageView imageView = (ImageView) viewRef.get();
if (imageView != null) {
width = getImageViewFieldValue(imageView, "mMaxWidth"); // 反射獲取,因為 getMaxWidth() 是API16開始才支持的
}
}
return width;
}
@Override
public int getHeight() {
int height = super.getHeight();
if (height <= 0) {
ImageView imageView = (ImageView) viewRef.get();
if (imageView != null) {
height = getImageViewFieldValue(imageView, "mMaxHeight");
}
}
return height;
}
那么我們的 QiyiDraweeView 也可以依葫蘆畫瓢
public int getWidthOrMaxWidth() {
int width = super.getWidth();
if (width <= 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
width = getMaxWidth();
} else {
width = getImageViewFieldValue("mMaxWidth"); // 反射獲取,因為 getMaxWidth() 是API16開始才支持的
}
if (width <= 0) {
width = getMeasuredWidth();
}
}
return width;
}
public int getHeightOrMaxHeight() {
int height = super.getHeight();
if (height <= 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
height = getMaxHeight();
} else {
height = getImageViewFieldValue("mMaxHeight");
}
if (height <= 0) {
height = getMeasuredHeight();
}
}
return height;
}
private int getImageViewFieldValue(String fieldName) {
int value = 0;
try {
Field field = ImageView.class.getDeclaredField(fieldName);
field.setAccessible(true);
int fieldValue = (Integer) field.get(this);
if (fieldValue > 0 && fieldValue < Integer.MAX_VALUE) {
value = fieldValue;
}
} catch (Exception e) {
}
return value;
}