Glide自定义ModelLoader来加载多张候选图片

Glide自定义ModelLoader来加载多张候选图片

需求

使用过Glide这个框架的同学大概知道,Glide从网络上加载图片时,只能加载一张图片,失败之后,可以选择显示占位图,如下面这段代码:

 Glide.with(context).load(url)
                .skipMemoryCache(true)
                .diskCacheStrategy(DiskCacheStrategy.DATA)
                .apply(new RequestOptions().dontAnimate())
                .placeholder(placeholderRes)
                .into(imageView);

但不知道大家有没有这样的需求:

如果我们有多张候选的图片url,需要在加载出错之后,尝试从下一个url中去进行加载,直到完成所有url的尝试。

所以我们希望在使用Glide加载图片时,大概的使用方式能如下面代码所示:

ImgUrlWrapper.java

/*
* ImgUrlWrapper类,将所有的url打包到一起
*/
public class ImgUrlWrapper {
	private List<String> urls;
	public ImgUrlWrapper(List<String> urls) {
		this.urls = urls;
	}
	public void setUrls(List<String> urls) {
		this.urls = urls;
	}

	public List<String> getUrls() {
		return this.urls;
	}
}
/*
* 一次图片加载失败之后,尝试加载其他的候选图片 
*/
 public static void displayWithCustomModule(Context context, ImageWrapper imageWrapper, int placeholderRes,
            ImageView imageView) {
        GlideApp.with(context)
                .load(imageWrapper)
                .skipMemoryCache(true)
                .diskCacheStrategy(DiskCacheStrategy.DATA).apply(new RequestOptions().dontAnimate())
                .placeholder(placeholderRes)
                .into(imageView);
    }

解题思路

读过Glide(version 4.11.0)源码的同学,大概能知道,Glide是通过ModelLoader<Model, Data>完成图片的加载过程的封装的,由DataFetcher<T>负责完成具体的数据加载,AppGlideModule负责注册一组组件,以在使用Glide的注释处理器时在应用程序中初始化Glide时使用。

/*
 * @param <Model> 资源的解码类型(转换前),比如:File, url/InputStream
 * @param <Data> 可用于ResourceDecoder解码资源的数据类型(转换后),比如:Bitmap, Drawable等等
 */
public interface ModelLoader<Model, Data> {

  /**
   * Contains a set of {@link com.bumptech.glide.load.Key Keys} identifying the source of the load,
   * alternate cache keys pointing to equivalent data, and a {@link
   * com.bumptech.glide.load.data.DataFetcher} that can be used to fetch data not found in cache.
   *
   * @param <Data> The type of data that well be loaded.
   */
  class LoadData<Data> {
    public final Key sourceKey;
    public final List<Key> alternateKeys;
    public final DataFetcher<Data> fetcher;

    public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
      this(sourceKey, Collections.<Key>emptyList(), fetcher);
    }

    public LoadData(
        @NonNull Key sourceKey,
        @NonNull List<Key> alternateKeys,
        @NonNull DataFetcher<Data> fetcher) {
      this.sourceKey = Preconditions.checkNotNull(sourceKey);
      this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
      this.fetcher = Preconditions.checkNotNull(fetcher);
    }
  }

  /**
   * 返回一个包裹了DataFetcher的LoadData, 即创建加载数据
   */
  @Nullable
  LoadData<Data> buildLoadData(
      @NonNull Model model, int width, int height, @NonNull Options options);

  /**
   * 返回次Loader是否能够处理对应Model的数据
   * @return true if this loader can handle the model and false otherwise
   */
  boolean handles(@NonNull Model model);
}

上面源码中通过buildLoadData方法创建LoadData,而LoadData中的DataFetcher如下:

/**
 * 负载加载数据的接口
 * @param <T> The type of data to be loaded (InputStream, byte[], File etc).
 */
public interface DataFetcher<T> {
  interface DataCallback<T> {

    /**
     * Called with the loaded data if the load succeeded, or with {@code null} if the load failed.
     */
    void onDataReady(@Nullable T data);

    /**
     * Called when the load fails.
     */
    void onLoadFailed(@NonNull Exception e);
  }

  /**
   * 从一个可以被解码的资源拉取数据
   */
  void loadData(@NonNull Priority priority, @NonNull DataCallback<? super T> callback);

  /**
   * 清理或回收资源
   */
  void cleanup();

  /**
  * 退出加载
   */
  void cancel();

  /** Returns the class of the data this fetcher will attempt to obtain. */
  @NonNull
  Class<T> getDataClass();

  /** Returns the {@link com.bumptech.glide.load.DataSource} this fetcher will return data from. */
  @NonNull
  DataSource getDataSource();
}

public abstract class AppGlideModule extends LibraryGlideModule implements AppliesOptions {

  public boolean isManifestParsingEnabled() {
    return true;
  }

  @Override
  public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
    // Default empty impl.
  }
}

public abstract class LibraryGlideModule implements RegistersComponents {
  @Override
  public void registerComponents(
      @NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
    // Default empty impl.
  }
}

实现过程

先定义一个Model(即ModelLoader<Model, Data>中的Model),我们把所有候选url都打包进去

public class ImgUrlWrapper {

    private List<String> mUrls;

    public ImgUrlWrapper(List<String> urls) {
        mUrls = urls;
    }

    public List<String> getUrls() {
        return mUrls;
    }

    public void setUrls(List<String> urls) {
        mUrls = urls;
    }
}

定义我们自己的DataFetcher,并参考Glide官方的HttpUrlFetcher来实现我们的需求(候选图片的加载)。我们在上面提到过,DataFetcher负责实现具体的数据加载过程。

下面代码中省略部分都是从HttpUrlFetcher拷贝过来的,我们主要关注loadData方法

public class UrlsFetcher implements DataFetcher<InputStream> {

    private static final String TAG = "HttpUrlFetcher";
    private static final int MAXIMUM_REDIRECTS = 5;
    private static final int INVALID_STATUS_CODE = -1;
    private final int timeout = 2500;
    private HttpUrlConnectionFactory connectionFactory;
    static HttpUrlConnectionFactory DEFAULT_CONNECTION_FACTORY =
            new DefaultHttpUrlConnectionFactory();
    private HttpURLConnection urlConnection;
    private InputStream stream;
    private volatile boolean isCancelled;
    private final List<String> mUrls;
   
    private int urlCount = 0;

    public UrlsFetcher(List<String> urls) {
        this.mUrls = urls;
        connectionFactory = DEFAULT_CONNECTION_FACTORY;
        urlCount = urls.size();
    }

    @Override
    public void loadData(@NonNull Priority priority,
            @NonNull DataCallback<? super InputStream> callback) {
        // 从最后一个图片url开始加载图片
        urlCount--;
        try {
            GlideUrl glideUrl = new GlideUrl(mUrls.get(urlCount));
            InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null,
                    glideUrl.getHeaders());
            // 图片加载完成,直接回调
            callback.onDataReady(result);
        } catch (IOException e) {
        	// 如果加载失败,尝试加载下一张图
            if (urlCount >= 0) {
                // 打印日志来验证是否尝试从其他地址加载图片
                Log.d(TAG, "retry next url " + mUrls.get(urlCount));
                loadData(priority, callback);
            } else {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Failed to load data for url", e);
                }
                // 所有图片加载失败的回调
                callback.onLoadFailed(e);
            }
        } 
    }
   ...
}

完成自定义的DataFetcher,我们定义一个ModelLoader来封装这个加载过程:

public class UrlsLoader implements ModelLoader<ImgUrlWrapper, InputStream> {
    @Nullable
    @Override
    public LoadData<InputStream> buildLoadData(@NonNull ImgUrlWrapper wrapper, int width, int height,
            @NonNull Options options) {
        // 返回LoadData
        return new LoadData<>(new ObjectKey(wrapper), new UrlsFetcher(wrapper.getUrls()));
    }

    @Override
    public boolean handles(@NonNull ImgUrlWrapper wrapper) {
   		// 只允许带有http和https的链接地址
        for (String url : wrapper.getUrls()) {
            if (!url.startsWith("http") || !url.startsWith("https")) {
                return false;
            }
        }
        return true;
    }

    public static class Factory implements ModelLoaderFactory<ImgUrlWrapper, InputStream> {
        @NonNull
        @Override
        public ModelLoader<ImgUrlWrapper, InputStream> build(
                @NonNull MultiModelLoaderFactory multiFactory) {
            return new UrlsLoader();
        }
        @Override
        public void teardown() {

        }
    }
}

完成下面最后一步,我们就可以验证我们的思路是否正确了

/*
* 注解@GlideModule不能缺,不然无法编译生成我们想要的GlideApp
* AppGlideModule中有个注释,如下:
* Classes that extend {@link AppGlideModule} must be annotated with {@link
 * com.bumptech.glide.annotation.GlideModule} to be processed correctly.
*/
@GlideModule
public class UrlsModule extends AppGlideModule {
    @Override
    public void registerComponents(@NonNull Context context, @NonNull Glide glide,
            @NonNull Registry registry) {
        super.registerComponents(context, glide, registry);
        // 完成注册
        glide.getRegistry().append(ImgUrlWrapper.class, InputStream.class, new UrlsLoader.Factory());
    }

    @Override
    public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
        super.applyOptions(context, builder);
    }
}

OK,我们使用写一个Demo来验证我们的想法是否正确:

public class MainActivity extends AppCompatActivity {

    private ImageView mImageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mImageView = findViewById(R.id.img);
         // 以百度搜索的Logo地址为例,下面所有地址中只有
        // https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png是正确的,
        // 其他地址都无法获取到图片
        List<String> urls = new ArrayList() {
            {
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png");
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d");
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6");
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6c");
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf");
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.p");
            add("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.pn");
            }
        };
        GlideApp.with(this.getApplicationContext())
                .load(new ImgUrlWrapper(urls))
                .skipMemoryCache(true)
                .diskCacheStrategy(DiskCacheStrategy.NONE)
                .apply(new RequestOptions().dontAnimate())
                .placeholder(R.mipmap.ic_launcher)
                .into(mImageView);
    }
}
2021-03-31 20:15:33.639 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.pn
2021-03-31 20:15:33.843 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.p
2021-03-31 20:15:34.051 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf
2021-03-31 20:15:34.226 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6c
2021-03-31 20:15:34.433 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6
2021-03-31 20:15:34.642 12972-12997/com.example.glidex D/HttpUrlFetcher: retry next url https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d

从上面的日志中,我们可以验证我们的思路和实现方式都是OK的。如果各位同学还有其他的想法,也可以借鉴这种思路来实现。

Demo源码

Gitee

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值