问题
为什么 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;
}