Реализуем работу Glide средствами Android

Автор: | 15.09.2017

Библиотеки в Android крайне полезны, поскольку выполняют внутри себя различные трудоёмкие задачи, освобождая разработчика от их реализации в своём приложении. Одной из таких библиотек является Glide.

Glide — это аналог другой известной библиотеки для изображений, Picasso, и на данный момент представляет собой одну из самых популярных библиотек для работы с изображениями, поскольку реализует асинхронную загрузку изображений, их обработку и кэширование. Кроме того, в отличие от Picasso, Glide умеет работать с GIF-изображениями и видео. Именно из-за этой особенности она использовалась в одном из наших приложений, которое скачивает файлы GIF из Интернета и показывает их пользователю. Причинами, по которым захотелось отказаться от Glide, явилось то, что сама по себе библиотека, обладая богатым функционалом, имеет довольно большой размер, что сказывается на итоговом размере APK-файла приложения.

Поэтому для минимизации размера приложения реализуем загрузку и обработку GIF файлов средствами, представленными в Android.

Интерфейс приложения состоит из RecyclerView, в который загружаются карточки, содержащие ImageView для отображения GIF файлов. Сами гифки хранятся на серверах Вконтакте и загружаются оттуда по GET-запросу от приложения.

При старте приложения на уровне Presenter главной активности отправляется запрос на получение данных о гифках (их количество, размер, ссылки для скачивания), которые затем помещаются в список, который будет использоваться адаптером RecyclerView. Каждая запись имеет две ссылки: на миниатюру и на собственно гифку.

private void newLoad(final int array_size) {
  CatApiInterface numberClient = ServiceGenerator.createService(CatApiInterface.class);
  catsNumberCall = numberClient.getCatsNumber();
  catsNumberCall.enqueue(new Callback<WallData>() {
    @Override public void onResponse(Call<WallData> call, Response<WallData> response) {
      if (!home_is_current_list) {
        return;
      }

      mCatsNumber = ((Double) response.body().responses.get(0)).intValue();
      int offset = mCatsNumber - (10 + array_size);

      if (offset == -10) {
        mainView.onItemsLoadComplete();
        return;
      }

      if (offset < 1) {
        offset = 0;
      }

      CatApiInterface client = ServiceGenerator.createService(CatApiInterface.class);
      dataCall = client.getCats("-120909644", "10", String.valueOf(offset));
      dataCall.enqueue(new Callback<Data>() {
        @Override public void onResponse(Call<Data> call, Response<Data> response) {
          if (response.isSuccessful()) {
            // request successful (status code 200, 201)
            Data result = response.body();
            LinkedHashSet<CatStore> newStore = new LinkedHashSet<>();

            for (Data.Response cat : result.responses) {
              newStore.add(new CatStore(cat.attachment.doc.url, cat.attachment.doc.thumb, false,
                  Integer.parseInt(cat.text.substring(0, cat.text.indexOf("X"))),
                  Integer.parseInt(cat.text.substring(cat.text.indexOf("X") + 1))));
            }

            for (CatStore cs : newStore) {
              downloadGifThread.queueThumb(cs.getThumb());
              downloadGifThread.queueGif(cs.getUrl());
            }

            newStore.addAll(mCatsSet);
            mCatsSet = newStore;
            mainView.onCatsLoaded(newStore, mCatsSet);
            mainView.onItemsLoadComplete();
          } else {
            mainView.onShowToast(mainView.getStringFromResource(R.string.error_loading));
            mainView.onItemsLoadComplete();
          }
        }

        @Override public void onFailure(Call<Data> call, Throwable t) {
          mainView.onShowToast(mainView.getStringFromResource(R.string.error_loading));
          mainView.onItemsLoadComplete();
        }
      });
    }

    @Override public void onFailure(Call<WallData> call, Throwable t) {
      mainView.onShowToast(mainView.getStringFromResource(R.string.error_loading));
      mainView.onItemsLoadComplete();
    }
  });
}

По окончанию загрузки Presenter сообщает View о том, что список загружен, после чего помещается в адаптер.

@Override
public void onCatsLoaded(LinkedHashSet<CatStore> newStore, LinkedHashSet<CatStore> catsSet) {
  if (mCatsAdapter != null) {
    mCatsAdapter.applyAndAnimateAdditions(new ArrayList<>(newStore));
  } else {
    setupRecyclerView(mRecyclerView, catsSet);
  }
}

Параллельно с формированием списка создаётся фоновый поток, который будет принимать от главного запросы на скачивание GIF-изображений. Фоновый поток наследует от класса HandlerThread и обеспечивает лучшее быстродействие, чем использование AsyncTask.

Handler responseHandler = new Handler();
downloadGifThread = new DownloadGifThread<>(responseHandler);
downloadGifThread.start();
downloadGifThread.getLooper();

Подробнее о том, как организован фоновый поток, можно почитать в этой статье.

Ссылки для скачиваний после записи в список сразу отправляются в фоновый поток для обработки. Там проверяется, находятся ли данные изображения в кэше, и если нет, то с помощью библиотеки OkHhtp происходит их скачивание. При успешном завершении миниатюры и гифки добавляются в кэш.

public class DownloadGifThread<T> extends HandlerThread {
  private static final String TAG = "DownloadGifThread";
  private static final int MESSAGE_DOWNLOAD_THUMB = 0;
  private static final int MESSAGE_DOWNLOAD_GIF = 1;
  private Handler mRequestHandler;
  private List<String> mRequestListThumb = new ArrayList<>();
  private List<String> mRequestListGif = new ArrayList<>();
  private Handler mResponseHandler;
  private GetGifThreadListener<T> mGetGifThreadListener;
  private GifCache gifCache;
  private ThumbnailCache thumbnailCache;

  public interface GetGifThreadListener<T> {
    void onGifDownloaded(GifDrawable gifDrawable, String gif);

    void onThumbDownloaded(Bitmap bitmapThumb, String thumb);
  }

  public void setDownloadGifListener(GetGifThreadListener<T> listener) {
    mGetGifThreadListener = listener;
  }

  public DownloadGifThread(Handler responseHandler) {
    super(TAG);
    this.mResponseHandler = responseHandler;
    this.gifCache = GifCache.getInstance();
    this.thumbnailCache = ThumbnailCache.getInstance();
  }

  @Override protected void onLooperPrepared() {
    mRequestHandler = new Handler() {
      @Override public void handleMessage(Message msg) {
        if (msg.what == MESSAGE_DOWNLOAD_THUMB) {
          String thumb = (String) msg.obj;
          handleRequestThumb(thumb);
        } else if (msg.what == MESSAGE_DOWNLOAD_GIF) {
          String gif = (String) msg.obj;
          handleRequestGif(gif);
        }
      }
    };
  }

  private void handleRequestThumb(final String thumb) {
    if (thumb != null && thumbnailCache.getThumbFromCache(thumb) == null) {
      try {
        Bitmap bitmapThumb = null;
        Response response = downloadTask(thumb);

        if (response != null && response.isSuccessful()) {
          bitmapThumb = BitmapFactory.decodeStream(response.body().byteStream());
          thumbnailCache.putThumbToCache(thumb, bitmapThumb);
        }

        final Bitmap bitmap = bitmapThumb;
        mResponseHandler.post(new Runnable() {
          @Override public void run() {
            int pos = mRequestListThumb.indexOf(thumb);
            if (mRequestListThumb.get(pos) == null || !mRequestListThumb.get(pos).equals(thumb)) {
              return;
            }

            mRequestListThumb.remove(pos);
            mGetGifThreadListener.onThumbDownloaded(bitmap, thumb);
          }
        });
      } catch (NullPointerException e) {
        e.printStackTrace();
      }
    }
  }

  private void handleRequestGif(final String gif) {
    if (gif != null && gifCache.getGifFromCache(gif) == null) {
      try {
        GifDrawable gifDrawable = null;
        Response response = downloadTask(gif);

        if (response != null && response.isSuccessful()) {
          gifDrawable = new GifDrawable(response.body().bytes());
          gifCache.putGifToCache(gif, gifDrawable);
        }

        final GifDrawable drawable = gifDrawable;
        mResponseHandler.post(new Runnable() {
          @Override public void run() {
            int pos = mRequestListGif.indexOf(gif);
            if (mRequestListGif.get(pos) == null || !mRequestListGif.get(pos).equals(gif)) {
              return;
            }

            mRequestListGif.remove(pos);
            mGetGifThreadListener.onGifDownloaded(drawable, gif);
          }
        });
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  private Response downloadTask(String url) {
    try {
      OkHttpClient client = new OkHttpClient();
      Request request = new Request.Builder().url(url).build();
      return client.newCall(request).execute();
    } catch (IOException e) {
      e.printStackTrace();
      return null;
    }
  }

  public void queueThumb(String thumb) {
    if (thumb == null) {
      mRequestListThumb.remove(thumb);
    } else {
      mRequestListThumb.add(thumb);
      mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD_THUMB, thumb).sendToTarget();
    }
  }

  public void queueGif(String gif) {
    if (gif == null) {
      mRequestListGif.remove(gif);
    } else {
      mRequestListGif.add(gif);
      mRequestHandler.obtainMessage(MESSAGE_DOWNLOAD_GIF, gif).sendToTarget();
    }
  }

  public void clearQueue() {
    mRequestHandler.removeMessages(MESSAGE_DOWNLOAD_THUMB);
    mRequestHandler.removeMessages(MESSAGE_DOWNLOAD_GIF);
  }
}

Кэш представляет собой два отдельных класса, ThumbnailCache и GifCache, наследующих от LruCache. Если объём такого кэша превышает допустимый объём, то из него автоматически удаляются те изображения, которые используются реже всего. Подробнее про LruCache можно почитать здесь.

Пока фоновый поток работает, адаптер RecyclerView инициализирует объекты в методе onBindViewHolder(). Внутри него подготавливаются размеры карточки в зависимости от GIF изображений, затем проверяется кэш на наличие в нём необходимых изображений. Если такие имеются — забираем из кэша и загружаем, если нет — добавляем запрос на загрузку в очередь фонового потока.

@Override public void onBindViewHolder(final RecyclerView.ViewHolder catsViewHolder, int i) {
  final CatStore item = catsList.get(i);
  CatsViewHolder rvei = (CatsViewHolder) catsViewHolder.itemView;
  rvei.image.setImageBitmap(null);
  rvei.prepareImage(item);

  boolean onlyWifi = (sp.getBoolean(SP.WIFI_ONLY, false) && !isWifiAvailable());

  ThumbnailCache thumbnailCache = ThumbnailCache.getInstance();
  GifCache gifCache = GifCache.getInstance();

  if (gifCache.getGifFromCache(item.getUrl()) != null) {
    rvei.bindGif(gifCache.getGifFromCache(item.getUrl()), item);
  } else {
    if (!onlyWifi) {
      downloadGifThread.queueGif(item.getUrl());
    }

    if (thumbnailCache.getThumbFromCache(item.getThumb()) != null) {
      rvei.bindThumb(thumbnailCache.getThumbFromCache(item.getThumb()));
    } else {
      if (!onlyWifi) {
        downloadGifThread.queueThumb(item.getThumb());
      }
    }
  }

  for (int pos = i - 10; pos <= i + 10; pos++) {
    if (pos >= 0 && pos < catsList.size()) {
      String thumb = catsList.get(pos).getThumb();

      if (thumbnailCache.getThumbFromCache(thumb) == null && !onlyWifi) {
        downloadGifThread.queueThumb(thumb);
      }

      String gif = catsList.get(pos).getUrl();

      if (gifCache.getGifFromCache(gif) == null && !onlyWifi) {
        downloadGifThread.queueGif(gif);
      }
    }
  }
}

Также в onBindViewHolder() происходит предварительная загрузка гифок. В цикле проверяет текущая позиция карточки и относительно её проверяется кэш на наличие изображений и отправляются запросы да загрузку.

К адаптеру привязан слушатель фонового потока, который при получении готовых изображений проверяет, какой именно ViewHolder его запросил, и вызывает onBindViewHolder() этого элемента.

@Override public void onGifDownloaded(GifDrawable gifDrawable, String gif) {
  if (catsList != null) {
    for (CatStore cs : catsList) {
      if (cs.getUrl().equals(gif)) {
        notifyItemChanged(catsList.indexOf(cs));
        return;
      }
    }
  }
}

@Override public void onThumbDownloaded(Bitmap bitmapThumb, String thumb) {
  if (catsList != null) {
    for (CatStore cs : catsList) {
      if (cs.getThumb().equals(thumb)) {
        notifyItemChanged(catsList.indexOf(cs));
        return;
      }
    }
  }
}

Для отображения на активности используется компонент GifImageView, предоставляемый библиотекой android-gif-drawable. Данная библиотека позволяет обрабатывать GIF изображения и корректно выводить их пользователю на экран. Можно реализовать отображение гифок и стандартными средствами, но это можно сделать позднее.

public void bindThumb(Bitmap bitmapThumb) {
  image.setImageBitmap(bitmapThumb);
}

public void bindGif(final GifDrawable gifDrawable, final CatStore catStore) {
  image.setImageDrawable(gifDrawable);
  image.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
      Intent intent = new Intent(context.getApplicationContext(), DetailActivity.class);
      Bundle bundle = new Bundle();
      bundle.putString("url", catStore.getUrl());
      bundle.putBoolean("selected", catStore.isSelected());
      bundle.putInt("height", catStore.getHeight());
      bundle.putInt("width", catStore.getWidth());
      intent.putExtras(bundle);

      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
        ActivityOptionsCompat options = ActivityOptionsCompat.
            makeSceneTransitionAnimation((Activity) context, image, "gif");
        context.startActivity(intent, options.toBundle());
      } else {
        context.startActivity(intent);
      }
    }
  });
}

Поскольку GifImageView наследует от обычного ImageView, ему доступны все его методы. В onClickListener() подготавливается намерение, которое откроется данную гифку на отдельной активности для полноэкранного просмотра. Загрузка изображения на этой активности аналогична адаптеру.

При переходе между активностями используется класс ActivityOptionsCompat, реализующий анимацию перехода изображения от позиции в списке в центр экрана и обратный переход при возврате из активности. Для того нужно в методе makeSceneTransitionAnimation() задать контекст, изображение, которое нужно анимировать, а также название элемента, по которому будет определяться, он это или нет. Имя элемента также нужно прописать в XML-файлах активностей, содержащих нужные GifImageView, например:

<pl.droidsonroids.gif.GifImageView
    android:id="@+id/imageView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:transitionName="gif"
    />

Для обратной анимации вместо обычного метода finish() для закрытия активности следует использовать метод supportFinishAfterTransition().

@Override public boolean onOptionsItemSelected(MenuItem item) {
  switch (item.getItemId()) {
    case R.id.action_share:
      shareGif();
      return true;
    case R.id.action_set_selected:
      detailPresenter.onSetSelectedClick();
      return true;
    case android.R.id.home:
      supportFinishAfterTransition();
      return true;
  }
  return super.onOptionsItemSelected(item);
}

Таким образом, мы удалили Glide из приложения и переписали загрузку и обработку гифок без его использования.

Библиотеки помогают решать распространённые задачи, не прикладывая к этому особых усилий, однако злоупотребление ими может сделать ваше приложение слишком тяжёлым, что также скажется на его работе.

Реализуем работу Glide средствами Android: 2 комментария

    1. Владимир

      Здравствуйте!
      По размеру apk нет особого прогресса (с использованием глайда — 3,6MB raw size, 3,1MB download size и без использование глайда — 3,6MB raw size, 3,2MB download size), поскольку в приложение добавлялись новые фичи. Однако по количеству классов и методов стало намного лучше: 5575 классов — 35952 метода с использованием глайда и 5238 классов — 34181 метод без использования глайда.

Добавить комментарий для Ivan Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *