Wstęp
Glide
jest wydajną biblioteką służącą do zarządzania multimediami, ich dekodowaniem i buforowaniem w pamięci oraz na dysku w celu ponownego szybkiego użycia. Skupia się przede wszystkim na płynnym i wydajnym wyświetlaniu, pobieraniu i modyfikowaniu obrazów. Ponadto obsługuje także obrazy animowane GIF oraz wideo. Zawiera elastyczne i prosty interfejs API, który umożliwia podłączanie niemal dowolnego klienta sieciowego (domyślnie jest to HttpUrlConnection).
Ładowanie
Pobieranie i ładowanie obrazów jest proste i może ograniczyć się zaledwie do jednego ciągu instrukcji. Wyrażenie with zwraca obiekt typu RequestBuilder
na którym należy wskazać źródło (load
) i typ zasobu, miejsce docelowe (into
) oraz opcjonalnie dokonać konfiguracji żądania. Możliwe jest także pobranie zasobu do wskazanego typu obietku z pominięciem ładowania do kontrolki widoku przez wskazanie jako miejsca docelowego obiektu typu Target
. Glide automatycznie wylicza rozmiar obrazu, aby dopasować go do widoku w którym będzie wyświetlany, jednakże jawnie ustawiony rozmiar przyspiesza proces przetwarzania. W trakcie niszczenia komponentu powiązane z nim zasoby są poddawane recyklingowi, a te nieużywany zostają usuwane.
private fun loadImage() {
//default load source as Drawable
Glide.with(this) //pass Context - returns RequestBuilder
.load(url) //pass url, file, drawable object, local resource etc
//set some RequestListener if needed
.into(imageView) //tell where to load image
}
private fun loadImageIntoBitmapTarget() {
val target = object: CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
val bitmap = resource
//do something with downloaded bitmap
}
override fun onLoadCleared(placeholder: Drawable?) {
//action when loading canceled
}
//implement other callback methods like onLoadStarted, onLoadFailed
}
//could be: asDrawable, asBitmap, asFile, asGif
Glide.with(this).asBitmap().load(url).into(target)
}
private fun clearData() {
//manual loading data clear
Glide.with(this).clear(imageView) //or pass target
}
Symbol zastępczy
Symbol zastępczy (placeholder
) jest graficznym wyświetlanym w trakcie pobierania i przetwarzania obiektu źródłowego. Gdy żądanie zostaje ukończone pozytywnie wówczas placeholder jest zastępowany przez źródło. Ładowanie odbywa się na wątku głównym, a transformacje są niedozwolone. Dodatkowo możliwe jest ustawienie obiektu placeholder dla żądania zakończonego kodem błędu (error
i fallback
), jednakże gdy nie zostanie on ustawiony wówczas pozostanie wyświetlany bieżący placeholder (jeśli został ustawiony). Ponadto możliwe jest ustawienie miniatury (thumbnail
), która jest pobierana równolegle z głównym żądaniem co pozwala na zwiększenie doświadczeń użytkownika przez wyświetlenia obrazu niższej rozdzielczości (zamiast zastępnika) w trakcie oczekiwania na pobranie pełnego obrazu.
private fun loadWithPlaceholders() {
Glide.with(this).load(url)
.placeholder(R.drawable.placeholder) //main placeholder
.error(R.drawable.placeholder_error) //placeholder when request permanently fails
.fallback(R.drawable.placeholder_fallback) //placeholder when requested model is null
.into(imageView)
}
private fun loadWithThumbnail() {
Glide.with(this).load(url)
.thumbnail(Glide.with(this).load(url_miniature))
.into(imageView)
}
Opcje
Glide oferuje wiele opcji przetwarzania i ładowania zasobów takich jak m.in. transformacje, przejścia, czy buforowanie które można zastosować dla wybranych żądań bezpośrednio na obiekcie RequestBuilder
. Opcje mogą być także współdzielone przez instancję RequestOptions
(transformacje i strategie buforowania) oraz TransitionOptions
(przejścia). Transformacje (transitions
) zwracają zmodyfikowany zasób i są używane przede wszystkim do przycinania obrazu i stosowania filtrów. Zastosowanie metody dowolnej transformacji zastępuje poprzednią dlatego w celu zaaplikowania kilku transformacji należy przekazać je do metody transform. Przejścia (transformations
) działa w kontekście pojedynczego żądania i pozwalają zdefiniować w jaki sposób Glide powinien przejść z obiektu zastępczego czy miniatury do załadowanego docelowego obrazu. Ponadto użycie przejść wpływa na wydajność w związku z czym należy rozważyć unikanie animacji przejść szczególnie w przypadku kolekcji.
private fun loadImageCustom() {
//load image with some custom modifications
Glide.with(this).load(url)
.centerCrop() //apply some transforms
.transition(withCrossFade())
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(imageView)
}
private fun loadImageCustomOptions() {
//TransitionOptions are tied with specific type like DrawableTransitionOptions
val transitionOptions = DrawableTransitionOptions()
.crossFade()
//create RequestOptions and reuse it
val requestOptions = RequestOptions()
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
//add more transforms
Glide.with(this).load(url)
.transition(transitionOptions)
.apply(requestOptions).into(imageView)
}
private fun loadImageMultipleTransforms() {
Glide.with(this).load(url)
.transform(FitCenter(), Rotate(90))
.into(imageView)
}
Pamięć
Zanim Glide rozpocznie pobieranie nowego zasobu dokonuje sprawdzenia warstw pamięci w celu jego odnalezienia i ponownego użycia co przebiega w następujących krokach:
1. Aktywne zasoby (obraz jest wyświetlany w innym widoku)
2. Pamięc podręczna (obraz został niedawno załadowany i pozostaje nadal w pamięci)
3. Zasób (obraz został wcześniej zdekodowany, przekształcony i zapisany w pamięci dysku)
4. Dane (dane z których uzyskano obraz zapisano wcześniej w pamięci podręcznej dysku)
Jeśli zasób nie został odnaleziony w żadnej warstwie pamięci wówczas zostaje on pobrany z oryginalnego źródła. Weryfikacja istnienia zasobu w warstwach pamięci odbywa się na podstawie wyszukiwania klucza składającego się z modelu (File
, Uri
, Url
itp.), opcjonalnej sygnatury dołączonej metodą signature
oraz parametrów zasobów takich jak m.in. wielkość, transformacje, opcje czy typ (dla kroków 1-3). Glide pozwala na uwzględnienie (onlyRetrieveFromCache
) lub pominięcie pamięci podręcznej (skipMemoryCache
) oraz dostarcza kilka strategii dla pamięci dysku (DiskCacheStrategy
), które umożliwiają wybór sposobu ładowania i zapisywania pobranych zasobów.
private fun loadImageWithCustomSignature() {
Glide.with(this).load(url)
.signature(ObjectKey("version")) //custom metadata if possible like last modified time
.into(imageView)
}
private fun loadAndSaveImageBySomeStrategy() {
Glide.with(this).load(url)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) //is default
.into(imageView)
/* use one of DiskStrategyCache:
AUTOMATIC - optimal strategy based on data source
DATA - stores original retrieved data in disk cache
RESOURCE - stores data in disk cache after decoding
ALL - remote data with both DATA and RESOURCE, local data only with RESOURCE
NONE - saves no data to disk cache */
}
private fun loadImageOnylIfExistsInCache() {
Glide.with(this).load(url)
.onlyRetrieveFromCache(true)
.into(imageView)
//if image doesn't exists in memory or disk cache then load fail
}
private fun loadImageSkippingCache() {
Glide.with(this).load(url)
.skipMemoryCache(true)
.into(imageView)
//skip memory cache
}
Wielkość pamięci jest automatycznie ustalana i może być modyfikowana w oparciu o klasę MemorySizeCalculator
. Tymczasowe zwiększenie pamięci podręcznej odbywa się za pomocą metody setMemoryCategory
, a ręczne czyszczenie pamięci podręcznej i dysku przy użyciu clearMemory
(na głównym wątku) oraz clearDiskCache
(na wątku pobocznym).
Interfejs API
Glide pozwala na rozszerzenie interfejsu API dzięki czemu możliwe jest ustawienie i nadpisanie domyślnych opcji globalnych dla żądań oraz dołączenie bibliotek integracyjnych. Aby wygenerować interfejs należy stworzyć klasę modułu AppGlideModule
opatrzoną adnotacją @GlideModule
oraz opcjonalnie klasę rozszerzeń @GlideExtension
z metodami statycznymi oznaczonymi jako @GlideOption
i @GlideType
.
@GlideModule
class CustomAppGlideModule : AppGlideModule() {
//this will apply as global settings
override fun applyOptions(context: Context, builder: GlideBuilder) {
//create custom options
val requestOptions = RequestOptions()
.circleCrop()
.diskCacheStrategy(DiskCacheStrategy.DATA)
val transitionOptions = DrawableTransitionOptions()
.crossFade()
//set custom memory size
val customMemoryCacheSize = (MemorySizeCalculator.Builder(context)
.build().memoryCacheSize * 1.1).toLong()
//apply some custom settings
builder
.setDefaultRequestOptions(requestOptions)
.setDefaultTransitionOptions(Drawable::class.java, transitionOptions)
.setLogLevel(Log.ERROR)
.setMemoryCache(LruResourceCache(customMemoryCacheSize))
}
//body can be also empty
}
@GlideExtension
object CustomGlideExtension {
//class with private constructor and static annotated methods
@GlideOption
@JvmStatic
fun smallCircle(options: BaseRequestOptions<*>): BaseRequestOptions<*> {
return options.circleCrop().override(100)
}
//more @GlideOption and @GlideType methods
}
Odwołanie do stworzonego modułu odbywa się domyślnie na instancji GlideApp
tworzonej przy budowaniu projektu. Nie wyklucza to jednak użycia w standardowy sposób za pomocą instancji Glide
.
private fun loadImageByCustomModule() {
//will draw circle crop 100px image with cross fade transition and custom log and cache settings
GlideApp.with(this).load(url)
.smallCircle() //new custom option
.into(imageView)
}
RecyclerView
Glide ułatwia współpracę z RecyclerView
(obrazy są ładowane wcześniej zgodnie z kierunkiem przesuwania) poprzez użycie RecyclerViewPreloader
jako obiektu słuchacza dla akcji przesuwania addOnScrollListener
. W połączeniu z odpowiednim rozmiarem obrazu i optymalną strategią pamięci pozwala na zmniejszenie liczby ładowanych obrazów dostarczając je wcześniej.
//usage of RecyclerViewPreloader in RecyclerView
class RecyclerViewActivity : AppCompatActivity() {
private lateinit var recyclerView: RecyclerView
private lateinit var preloader: RecyclerViewPreloader<Any>
private val urls = listOf("url1", "url2", "url3") //mock some url
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recyclerView)
initPreloader()
initRecyclerView()
}
private fun initPreloader() {
val size = 100
val maxPreload = 10
val sizeProvider: ListPreloader.PreloadSizeProvider<Any> = FixedPreloadSizeProvider(size, size)
val modelProvider = CustomPreloadModelProvider(urls, this) //own implementation of PreloadModelProvider
preloader = RecyclerViewPreloader(this, modelProvider, sizeProvider, maxPreload)
}
private fun initRecyclerView() {
recyclerView = findViewById(R.id.recyclerView)
recyclerView.apply {
layoutManager = LinearLayoutManager(this@MainActivity)
adapter = CustomAdapter(urls, this@MainActivity)
addOnScrollListener(preloader) //set RecyclerViewPreloader as scroll listener
}
}
}
//implement PreloadModelProvider
class CustomPreloadModelProvider(val urls: List<String>, val context: Context) : ListPreloader.PreloadModelProvider<Any> {
override fun getPreloadItems(position: Int): MutableList<Any> {
val url = urls[position]
if (TextUtils.isEmpty(url)) {
return Collections.emptyList()
}
return Collections.singletonList(url)
}
override fun getPreloadRequestBuilder(item: Any): RequestBuilder<*>? {
return GlideApp.with(context).load(item)
}
}
//just simple adapter
class CustomAdapter(val urls: List<String>, val context: Context) : RecyclerView.Adapter<CustomAdapter.CustomViewHolder>() {
class CustomViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val imageView: ImageView
init {
imageView = view.findViewById(R.id.imageView)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
return CustomViewHolder(view)
}
override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
//set some data on item
GlideApp.with(context).load(urls[position]).into(holder.imageView) //standard Glide loading
}
override fun getItemCount(): Int = urls.size
}